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>
148 lines
4.3 KiB
C++
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
|