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

148 lines
4.3 KiB
C++

#include "common/endpoint.h"
#include <arpa/inet.h>
#include <charconv>
#include <cstring>
#include <functional>
namespace ovg {
std::optional<IpAddress> IpAddress::parse(const std::string &text) {
IpAddress out;
uint8_t buf[16];
if (text.find(':') == std::string::npos) {
if (inet_pton(AF_INET, text.c_str(), buf) == 1) {
std::memcpy(out.bytes_.data(), buf, 4);
out.v4_ = true;
out.valid_ = true;
return out;
}
return std::nullopt;
}
if (inet_pton(AF_INET6, text.c_str(), buf) == 1) {
std::memcpy(out.bytes_.data(), buf, 16);
out.v4_ = false;
out.valid_ = true;
return out;
}
return std::nullopt;
}
IpAddress IpAddress::from_v4(uint32_t host_order) {
IpAddress a;
a.bytes_[0] = static_cast<uint8_t>((host_order >> 24) & 0xff);
a.bytes_[1] = static_cast<uint8_t>((host_order >> 16) & 0xff);
a.bytes_[2] = static_cast<uint8_t>((host_order >> 8) & 0xff);
a.bytes_[3] = static_cast<uint8_t>(host_order & 0xff);
a.v4_ = true;
a.valid_ = true;
return a;
}
IpAddress IpAddress::from_bytes_v4(const uint8_t bytes[4]) {
IpAddress a;
std::memcpy(a.bytes_.data(), bytes, 4);
a.v4_ = true;
a.valid_ = true;
return a;
}
IpAddress IpAddress::from_bytes_v6(const uint8_t bytes[16]) {
IpAddress a;
std::memcpy(a.bytes_.data(), bytes, 16);
a.v4_ = false;
a.valid_ = true;
return a;
}
uint32_t IpAddress::v4_host_order() const {
return (static_cast<uint32_t>(bytes_[0]) << 24) |
(static_cast<uint32_t>(bytes_[1]) << 16) |
(static_cast<uint32_t>(bytes_[2]) << 8) |
static_cast<uint32_t>(bytes_[3]);
}
std::string IpAddress::to_string() const {
if (!valid_) return "<invalid>";
char buf[INET6_ADDRSTRLEN] = {};
if (v4_) {
inet_ntop(AF_INET, bytes_.data(), buf, sizeof(buf));
} else {
inet_ntop(AF_INET6, bytes_.data(), buf, sizeof(buf));
}
return buf;
}
Endpoint::Endpoint(IpAddress addr, uint16_t port)
: kind_(addr.is_v4() ? Kind::Ipv4 : Kind::Ipv6),
addr_(std::move(addr)),
port_(port) {}
Endpoint::Endpoint(std::string domain, uint16_t port)
: kind_(Kind::Domain), domain_(std::move(domain)), port_(port) {}
std::optional<Endpoint> Endpoint::parse(const std::string &text) {
if (text.empty()) return std::nullopt;
std::string host;
std::string port_str;
if (text.front() == '[') {
// Bracketed IPv6 literal: [::1]:80
const auto close = text.find(']');
if (close == std::string::npos) return std::nullopt;
host = text.substr(1, close - 1);
if (close + 1 >= text.size() || text[close + 1] != ':') return std::nullopt;
port_str = text.substr(close + 2);
} else {
const auto colon = text.rfind(':');
if (colon == std::string::npos) return std::nullopt;
// A bare IPv6 literal has several colons and no port; reject it as
// ambiguous rather than silently truncating the address.
if (text.find(':') != colon) return std::nullopt;
host = text.substr(0, colon);
port_str = text.substr(colon + 1);
}
if (host.empty() || port_str.empty()) return std::nullopt;
unsigned long port_val = 0;
const char *begin = port_str.data();
const char *end = begin + port_str.size();
const auto res = std::from_chars(begin, end, port_val);
if (res.ec != std::errc{} || res.ptr != end || port_val == 0 ||
port_val > 65535)
return std::nullopt;
if (auto ip = IpAddress::parse(host))
return Endpoint(*ip, static_cast<uint16_t>(port_val));
return Endpoint(std::move(host), static_cast<uint16_t>(port_val));
}
std::string Endpoint::host_string() const {
return kind_ == Kind::Domain ? domain_ : addr_.to_string();
}
std::string Endpoint::to_string() const {
const std::string h = host_string();
if (kind_ == Kind::Ipv6) return "[" + h + "]:" + std::to_string(port_);
return h + ":" + std::to_string(port_);
}
size_t EndpointHash::operator()(const Endpoint &e) const noexcept {
size_t h = std::hash<uint16_t>{}(e.port());
h ^= std::hash<int>{}(static_cast<int>(e.kind())) + 0x9e3779b9 + (h << 6) +
(h >> 2);
if (e.is_domain()) {
h ^= std::hash<std::string>{}(e.domain()) + 0x9e3779b9 + (h << 6) + (h >> 2);
} else {
const auto &b = e.address().bytes();
for (size_t i = 0; i < e.address().byte_len(); ++i)
h ^= std::hash<uint8_t>{}(b[i]) + 0x9e3779b9 + (h << 6) + (h >> 2);
}
return h;
}
} // namespace ovg