// 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 #include #include #include #include #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 users; }; struct VpnGateConfig { std::vector 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 country_allow; // empty = all std::vector 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 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 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 *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