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

197 lines
6.2 KiB
C++

// Configuration model and INI-style loader.
//
// Format:
// [section]
// key = value # comment
// list_key = a, b, c
//
// Durations accept a unit suffix: "30s", "5m", "1h", "250ms". Bare numbers are
// seconds. Keeping the format boring is intentional -- config bugs at 3am are
// worse than a missing feature.
#pragma once
#include <chrono>
#include <cstdint>
#include <map>
#include <string>
#include <vector>
#include "common/logging.h"
namespace ovg {
using Millis = std::chrono::milliseconds;
struct Credential {
std::string username;
// Stored as sha256(salt || password), hex. Never the plaintext.
std::string salt_hex;
std::string hash_hex;
};
struct Socks5Config {
std::string listen_address = "127.0.0.1";
uint16_t listen_port = 1080;
bool require_auth = true;
// Address handed back in the UDP ASSOCIATE reply. Must be reachable *by the
// client*, which is why it cannot just be the listen address when that is
// 0.0.0.0. Empty means "derive from the control connection's local address".
std::string advertise_address;
bool udp_associate_enabled = true;
size_t max_sessions = 1200;
Millis handshake_timeout{10000};
Millis connect_timeout{20000};
Millis idle_timeout{300000};
Millis udp_idle_timeout{60000};
size_t relay_buffer_size = 16384;
int io_threads = 0; // 0 = min(hardware_concurrency, 4)
std::string auth_file;
std::vector<Credential> users;
};
struct VpnGateConfig {
std::vector<std::string> api_urls{
"http://www.vpngate.net/api/iphone/",
"http://www.vpngate.net/api/iphone/", // retried; mirrors can be added
};
Millis refresh_interval{1800000}; // 30m
Millis http_timeout{30000};
std::string cache_path = "var/vpngate_cache.csv";
Millis cache_max_age{21600000}; // 6h -- stale cache still beats no nodes
size_t max_response_bytes = 32u * 1024 * 1024;
};
struct SelectorConfig {
std::vector<std::string> country_allow; // empty = all
std::vector<std::string> country_deny;
bool prefer_udp = true;
size_t probe_candidates = 12;
size_t probe_samples = 3;
Millis probe_timeout{3000};
size_t probe_concurrency = 8;
// Prior (API-derived) weights.
double w_score = 0.35;
double w_speed = 0.30;
double w_sessions = 0.20;
double w_uptime = 0.15;
// Final blend.
double w_rtt = 0.45;
double w_prior = 0.30;
double w_history = 0.25;
std::string history_path = "var/node_history.tsv";
Millis failure_backoff_initial{60000};
Millis failure_backoff_max{3600000};
};
struct SwitchConfig {
enum class Mode {
Graceful, // make-before-break with a bounded drain window
Hard, // promote and immediately close every old session
};
Mode mode = Mode::Graceful;
Millis drain_grace{120000};
size_t max_draining = 2;
Millis min_interval{60000};
double improvement_margin = 0.20; // candidate must beat current by this much
Millis backoff_initial{30000};
Millis backoff_max{480000};
bool retry_zero_progress = true; // transparently re-home untouched sessions
bool rehome_udp = true; // UDP associations survive a switch
// How often to go looking for a *better* node while the current one is
// perfectly healthy. Off by default, and deliberately so: a scan probes a
// dozen volunteer-run servers, and switching a healthy tunnel costs every
// session that has moved bytes. Degradation-driven switching (health/) is
// what the requirement actually asks for; this is the optional upgrade path.
Millis opportunistic_interval{0}; // 0 = disabled
};
struct HealthConfig {
Millis interval{15000};
int unhealthy_windows = 3;
Millis probe_timeout{5000};
// The probe dials this host:port *through the egress* and drops the stream
// as soon as it is up. A TCP handshake is used rather than a bare DNS lookup
// because a lookup can be answered from the resolver cache without a single
// byte crossing the tunnel -- which would report a dead tunnel as healthy.
std::string probe_domain = "www.google.com";
uint16_t probe_port = 80;
double min_score = 0.40;
double max_connect_failure_rate = 0.50;
Millis stall_threshold{45000};
};
struct OvpnConfig {
bool allow_legacy_algorithms = true; // VPNGate is AES-128-CBC / SHA1
std::string username = "vpn"; // fallback for nodes that demand it
std::string password = "vpn";
int connect_timeout_s = 30;
bool compression = true;
int tunnel_up_timeout_s = 45;
// SO_SNDBUF/SO_RCVBUF for the tun socketpair; too small drops IP packets
// under burst (recoverable, but hurts throughput).
int packet_socket_buffer = 2 * 1024 * 1024;
};
struct DnsConfig {
std::vector<std::string> fallback_servers{"1.1.1.1", "8.8.8.8"};
Millis timeout{5000};
size_t cache_entries = 4096;
Millis min_ttl{5000};
Millis max_ttl{3600000};
bool prefer_ipv4 = true;
};
struct AdminConfig {
bool enabled = true;
std::string listen_address = "127.0.0.1";
uint16_t listen_port = 9080;
};
struct LogConfig {
log::Level level = log::Level::Info;
std::string file = "-";
std::map<std::string, log::Level> module_levels;
};
struct Config {
Socks5Config socks5;
VpnGateConfig vpngate;
SelectorConfig selector;
SwitchConfig switching;
HealthConfig health;
OvpnConfig ovpn;
DnsConfig dns;
AdminConfig admin;
LogConfig logging;
// Egress backend. "tunnel" = OpenVPN+lwIP, "direct" = host sockets (testing).
std::string egress_mode = "tunnel";
// Loads and validates. Returns false and fills `err` on any problem; a
// partially-applied config is never returned.
static bool load_file(const std::string &path, Config *out, std::string *err);
static bool load_string(const std::string &text, Config *out,
std::string *err);
bool validate(std::string *err) const;
};
// Parses "250ms" / "30s" / "5m" / "2h"; bare numbers are seconds.
bool parse_duration(const std::string &text, Millis *out);
// Loads "user:password" or "user:sha256$salt$hash" lines.
bool load_auth_file(const std::string &path, std::vector<Credential> *out,
std::string *err);
// Builds a credential with a fresh random salt.
Credential make_credential(const std::string &user, const std::string &password);
// Constant-time verification.
bool verify_credential(const Credential &c, const std::string &password);
} // namespace ovg