#include "common/config.h" #include "common/endpoint.h" #include "common/error.h" #include "common/metrics.h" #include "harness.h" using namespace ovg; // --------------------------------------------------------------------------- // Endpoint OVG_TEST(IpAddressParseV4) { auto a = IpAddress::parse("192.168.1.1"); CHECK(a.has_value()); CHECK(a->is_v4()); CHECK_EQ(a->to_string(), std::string("192.168.1.1")); CHECK_EQ(a->v4_host_order(), uint32_t(0xC0A80101)); CHECK_EQ(a->byte_len(), size_t(4)); } OVG_TEST(IpAddressParseV6) { auto a = IpAddress::parse("2001:db8::1"); CHECK(a.has_value()); CHECK(a->is_v6()); CHECK_EQ(a->to_string(), std::string("2001:db8::1")); CHECK_EQ(a->byte_len(), size_t(16)); } OVG_TEST(IpAddressRejectsGarbage) { CHECK(!IpAddress::parse("example.com").has_value()); CHECK(!IpAddress::parse("999.1.1.1").has_value()); CHECK(!IpAddress::parse("").has_value()); } OVG_TEST(IpAddressFromBytes) { const uint8_t b[4] = {8, 8, 4, 4}; auto a = IpAddress::from_bytes_v4(b); CHECK_EQ(a.to_string(), std::string("8.8.4.4")); CHECK_EQ(a, *IpAddress::parse("8.8.4.4")); } OVG_TEST(EndpointParseForms) { auto v4 = Endpoint::parse("1.2.3.4:80"); CHECK(v4.has_value()); CHECK(v4->kind() == Endpoint::Kind::Ipv4); CHECK_EQ(v4->port(), uint16_t(80)); CHECK_EQ(v4->to_string(), std::string("1.2.3.4:80")); auto v6 = Endpoint::parse("[::1]:8080"); CHECK(v6.has_value()); CHECK(v6->kind() == Endpoint::Kind::Ipv6); CHECK_EQ(v6->port(), uint16_t(8080)); CHECK_EQ(v6->to_string(), std::string("[::1]:8080")); auto dom = Endpoint::parse("example.com:443"); CHECK(dom.has_value()); CHECK(dom->is_domain()); CHECK_EQ(dom->domain(), std::string("example.com")); CHECK_EQ(dom->host_string(), std::string("example.com")); } OVG_TEST(EndpointDomainStaysUnresolved) { // The whole point of keeping Domain as a kind: no local DNS lookup happens, // so nothing leaks outside the tunnel. Endpoint e("example.com", 443); CHECK(e.is_domain()); CHECK(!e.address().valid()); } OVG_TEST(EndpointRejectsBadInput) { CHECK(!Endpoint::parse("1.2.3.4").has_value()); // no port CHECK(!Endpoint::parse("1.2.3.4:99999").has_value()); // port out of range CHECK(!Endpoint::parse("").has_value()); } // --------------------------------------------------------------------------- // Errors OVG_TEST(ErrorCodesMapToSocks5Replies) { auto rep = [](Error e) { return int(socks5_reply_for(make_error_code(e))); }; CHECK_EQ(rep(Error::Ok), 0x00); CHECK_EQ(rep(Error::ConnectionRefused), 0x05); CHECK_EQ(rep(Error::NetworkUnreachable), 0x03); CHECK_EQ(rep(Error::HostUnreachable), 0x04); CHECK_EQ(rep(Error::ResolveFailed), 0x04); CHECK_EQ(rep(Error::Timeout), 0x06); // TTL expired CHECK_EQ(rep(Error::NotSupported), 0x07); // A dead or draining egress looks like an unreachable network to the client. CHECK_EQ(rep(Error::EgressDraining), 0x03); CHECK_EQ(rep(Error::EgressGone), 0x03); // Anything unmapped must still be a valid REP value, not a random byte. CHECK_EQ(rep(Error::Internal), 0x01); } OVG_TEST(Socks5ReplyMapsSystemErrors) { // asio hands us std::errc, not our category. CHECK_EQ(int(socks5_reply_for(std::make_error_code( std::errc::connection_refused))), 0x05); CHECK_EQ(int(socks5_reply_for(std::make_error_code(std::errc::timed_out))), 0x06); CHECK_EQ(int(socks5_reply_for(std::error_code())), 0x00); } OVG_TEST(ErrorCodesHaveMessages) { const std::error_code ec = Error::EgressDraining; // implicit conversion CHECK(ec); // non-zero CHECK(!ec.message().empty()); CHECK(!make_error_code(Error::Ok)); } // --------------------------------------------------------------------------- // Metrics OVG_TEST(MetricsHandlesAreStable) { auto *a = metrics::counter("ovg_test_thing_total", "help"); auto *b = metrics::counter("ovg_test_thing_total"); CHECK_EQ(a, b); a->inc(3); CHECK_EQ(b->value(), uint64_t(3)); } OVG_TEST(MetricsRenderPrometheus) { metrics::gauge("ovg_test_gauge", "a gauge")->set(42); const auto text = metrics::Registry::instance().render_prometheus(); CHECK_NE(text.find("# TYPE ovg_test_gauge gauge"), std::string::npos); CHECK_NE(text.find("ovg_test_gauge 42"), std::string::npos); } // --------------------------------------------------------------------------- // Config OVG_TEST(ParseDurationUnits) { Millis m{}; CHECK(parse_duration("250ms", &m)); CHECK_EQ(m.count(), int64_t(250)); CHECK(parse_duration("30s", &m)); CHECK_EQ(m.count(), int64_t(30000)); CHECK(parse_duration("5m", &m)); CHECK_EQ(m.count(), int64_t(300000)); CHECK(parse_duration("2h", &m)); CHECK_EQ(m.count(), int64_t(7200000)); CHECK(parse_duration("1d", &m)); CHECK_EQ(m.count(), int64_t(86400000)); // Bare number = seconds. CHECK(parse_duration("45", &m)); CHECK_EQ(m.count(), int64_t(45000)); } OVG_TEST(ParseDurationRejectsGarbage) { Millis m{}; CHECK(!parse_duration("", &m)); CHECK(!parse_duration("soon", &m)); CHECK(!parse_duration("-5s", &m)); CHECK(!parse_duration("5 fortnights", &m)); } OVG_TEST(ConfigDefaultsRefuseAnonymousProxy) { // Secure by default: auth is on and there are no users, so a config that // defines neither must be rejected rather than quietly opening an open relay. Config c; std::string err; CHECK(!c.validate(&err)); CHECK_NE(err.find("require_auth"), std::string::npos); CHECK_EQ(c.socks5.listen_port, uint16_t(1080)); CHECK_EQ(c.socks5.listen_address, std::string("127.0.0.1")); CHECK(c.socks5.require_auth); CHECK(c.socks5.udp_associate_enabled); // With a user it validates. c.socks5.users.push_back(make_credential("alice", "hunter2")); CHECK(c.validate(&err)); } OVG_TEST(ConfigLoadsSections) { const char *text = R"( # a comment [socks5] listen_address = 0.0.0.0 listen_port = 1081 advertise_address = 203.0.113.7 idle_timeout = 90s max_sessions = 2000 [selector] country_allow = JP, KR, SG prefer_udp = false probe_candidates = 20 [switch] mode = hard drain_grace = 30s [users] alice = hunter2 )"; Config c; std::string err; CHECK(Config::load_string(text, &c, &err)); CHECK_EQ(err, std::string("")); CHECK_EQ(c.socks5.listen_address, std::string("0.0.0.0")); CHECK_EQ(c.socks5.listen_port, uint16_t(1081)); CHECK_EQ(c.socks5.idle_timeout.count(), int64_t(90000)); CHECK_EQ(c.socks5.max_sessions, size_t(2000)); CHECK_EQ(c.selector.country_allow.size(), size_t(3)); CHECK_EQ(c.selector.country_allow[2], std::string("SG")); CHECK(!c.selector.prefer_udp); CHECK_EQ(c.selector.probe_candidates, size_t(20)); CHECK(c.switching.mode == SwitchConfig::Mode::Hard); CHECK_EQ(c.switching.drain_grace.count(), int64_t(30000)); CHECK_EQ(c.socks5.users.size(), size_t(1)); CHECK_EQ(c.socks5.users[0].username, std::string("alice")); CHECK(verify_credential(c.socks5.users[0], "hunter2")); } OVG_TEST(ConfigRejectsUnknownKey) { // A key nobody reads is a key that silently does nothing. Catch it at load. Config c; std::string err; CHECK(!Config::load_string( "[socks5]\nlisten_prot = 1080\n[users]\na = b\n", &c, &err)); CHECK_NE(err.find("socks5.listen_prot"), std::string::npos); } OVG_TEST(ConfigAcceptsUserDefinedSections) { // [users] and [log.modules] have keys we cannot enumerate; they must not be // mistaken for typos by the unknown-key check. Config c; std::string err; CHECK(Config::load_string( "[log.modules]\nsocks5 = debug\nselector = warn\n" "[users]\nalice = a\nbob = b\n", &c, &err)); CHECK_EQ(err, std::string("")); CHECK_EQ(c.socks5.users.size(), size_t(2)); CHECK(c.logging.module_levels.at("socks5") == log::Level::Debug); CHECK(c.logging.module_levels.at("selector") == log::Level::Warn); } OVG_TEST(ConfigRejectsBadValues) { Config c; std::string err; CHECK(!Config::load_string("[socks5]\nlisten_port = eighty\n[users]\na=b\n", &c, &err)); CHECK(!Config::load_string("[switch]\nmode = sideways\n[users]\na=b\n", &c, &err)); CHECK(!Config::load_string("[dns]\nfallback_servers = dns.example.com\n" "[users]\na=b\n", &c, &err)); CHECK_NE(err.find("IP literals"), std::string::npos); } OVG_TEST(CredentialsAreSaltedAndVerify) { auto c = make_credential("alice", "hunter2"); CHECK_EQ(c.username, std::string("alice")); CHECK(!c.salt_hex.empty()); CHECK_EQ(c.hash_hex.size(), size_t(64)); // sha256 hex CHECK(verify_credential(c, "hunter2")); CHECK(!verify_credential(c, "hunter3")); CHECK(!verify_credential(c, "")); // Same password, different salt => different hash. auto d = make_credential("alice", "hunter2"); CHECK_NE(c.salt_hex, d.salt_hex); CHECK_NE(c.hash_hex, d.hash_hex); }