Skip to content

Commit f04b0d2

Browse files
committed
evo: allow ExtNetInfo to accept DomainPort if PLATFORM_HTTPS
1 parent 79cc52c commit f04b0d2

File tree

3 files changed

+107
-4
lines changed

3 files changed

+107
-4
lines changed

src/evo/netinfo.cpp

Lines changed: 81 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,46 @@ static constexpr uint8_t DOMAIN_MAX_LEN{253};
2525
/** Minimum length of a FQDN */
2626
static constexpr uint8_t DOMAIN_MIN_LEN{3};
2727

28+
static constexpr std::string_view SAFE_CHARS_ALPHA{"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"};
2829
static constexpr std::string_view SAFE_CHARS_IPV4{"1234567890."};
2930
static constexpr std::string_view SAFE_CHARS_IPV4_6{"abcdefABCDEF1234567890.:[]"};
3031
static constexpr std::string_view SAFE_CHARS_RFC1035{"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.-"};
32+
static constexpr std::array<std::string_view, 13> TLDS_BAD{
33+
// ICANN resolution 2018.02.04.12
34+
".mail",
35+
// Infrastructure TLD
36+
".arpa",
37+
// RFC 6761
38+
".example", ".invalid", ".localhost", ".test",
39+
// RFC 6762
40+
".local",
41+
// RFC 6762, Appendix G
42+
".corp", ".home", ".internal", ".intranet", ".lan", ".private",
43+
};
3144

3245
bool MatchCharsFilter(std::string_view input, std::string_view filter)
3346
{
3447
return std::all_of(input.begin(), input.end(), [&filter](char c) { return filter.find(c) != std::string_view::npos; });
3548
}
49+
50+
bool MatchSuffix(const std::string& str, Span<const std::string_view> list)
51+
{
52+
if (str.empty()) return false;
53+
for (const auto& suffix : list) {
54+
if (suffix.size() > str.size()) continue;
55+
if (std::string_view{str}.ends_with(suffix)) return true;
56+
}
57+
return false;
58+
}
59+
60+
bool IsAllowedPlatformHTTPPort(uint16_t port)
61+
{
62+
switch (port) {
63+
case 443:
64+
return true;
65+
}
66+
return false;
67+
}
3668
} // anonymous namespace
3769

3870
bool IsNodeOnMainnet() { return Params().NetworkIDString() == CBaseChainParams::MAIN; }
@@ -387,6 +419,10 @@ NetInfoStatus ExtNetInfo::ProcessCandidate(const NetInfoPurpose purpose, const N
387419
if (IsAddrPortDuplicate(candidate)) {
388420
return NetInfoStatus::Duplicate;
389421
}
422+
if (candidate.GetDomainPort().has_value() && purpose != NetInfoPurpose::PLATFORM_HTTPS) {
423+
// Domains only allowed for Platform HTTPS API
424+
return NetInfoStatus::BadInput;
425+
}
390426
if (auto it{m_data.find(purpose)}; it != m_data.end()) {
391427
// Existing entries list found, check limit
392428
auto& [_, entries] = *it;
@@ -426,6 +462,26 @@ NetInfoStatus ExtNetInfo::ValidateService(const CService& service)
426462
return NetInfoStatus::Success;
427463
}
428464

465+
NetInfoStatus ExtNetInfo::ValidateDomainPort(const DomainPort& domain)
466+
{
467+
if (!domain.IsValid()) {
468+
return NetInfoStatus::BadInput;
469+
}
470+
const uint16_t domain_port{domain.GetPort()};
471+
if (domain_port == 0 || (IsBadPort(domain_port) && !IsAllowedPlatformHTTPPort(domain_port))) {
472+
return NetInfoStatus::BadPort;
473+
}
474+
const std::string& addr{domain.ToStringAddr()};
475+
if (MatchSuffix(addr, TLDS_BAD)) {
476+
return NetInfoStatus::BadInput;
477+
}
478+
if (const auto labels{SplitString(addr, '.')}; !MatchCharsFilter(labels.at(labels.size() - 1), SAFE_CHARS_ALPHA)) {
479+
return NetInfoStatus::BadInput;
480+
}
481+
482+
return NetInfoStatus::Success;
483+
}
484+
429485
NetInfoStatus ExtNetInfo::AddEntry(const NetInfoPurpose purpose, const std::string& input)
430486
{
431487
if (!IsValidPurpose(purpose)) {
@@ -437,11 +493,25 @@ NetInfoStatus ExtNetInfo::AddEntry(const NetInfoPurpose purpose, const std::stri
437493
std::string addr;
438494
uint16_t port{0};
439495
SplitHostPort(input, port, addr);
440-
// Contains invalid characters, unlikely to pass Lookup(), fast-fail
496+
441497
if (!MatchCharsFilter(addr, SAFE_CHARS_IPV4_6)) {
442-
return NetInfoStatus::BadInput;
498+
if (!MatchCharsFilter(addr, SAFE_CHARS_RFC1035)) {
499+
// Neither IP:port safe nor domain-safe, we can safely assume it's bad input
500+
return NetInfoStatus::BadInput;
501+
}
502+
503+
// Not IP:port safe but domain safe, treat as domain.
504+
if (DomainPort domain; domain.Set(addr, port) == DomainPort::Status::Success) {
505+
const auto ret{ValidateDomainPort(domain)};
506+
if (ret == NetInfoStatus::Success) {
507+
return ProcessCandidate(purpose, NetInfoEntry{domain});
508+
}
509+
return ret; /* ValidateDomainPort() failed */
510+
}
511+
return NetInfoStatus::BadInput; /* DomainPort::Set() failed */
443512
}
444513

514+
// IP:port safe, try to parse it as IP:port
445515
if (auto service_opt{Lookup(addr, /*portDefault=*/port, /*fAllowLookup=*/false)}) {
446516
const auto ret{ValidateService(*service_opt)};
447517
if (ret == NetInfoStatus::Success) {
@@ -516,6 +586,15 @@ NetInfoStatus ExtNetInfo::Validate() const
516586
// Stores CService underneath but doesn't pass validation rules
517587
return ret;
518588
}
589+
} else if (const auto domain_opt{entry.GetDomainPort()}) {
590+
if (purpose != NetInfoPurpose::PLATFORM_HTTPS) {
591+
// Domains only allowed for Platform HTTPS API
592+
return NetInfoStatus::BadInput;
593+
}
594+
if (auto ret{ValidateDomainPort(*domain_opt)}; ret != NetInfoStatus::Success) {
595+
// Stores DomainPort underneath but doesn't pass validation rules
596+
return ret;
597+
}
519598
} else {
520599
// Doesn't store valid type underneath
521600
return NetInfoStatus::Malformed;

src/evo/netinfo.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,7 @@ class ExtNetInfo final : public NetInfoInterface
384384

385385
/** Validate CService candidate address against ruleset */
386386
static NetInfoStatus ValidateService(const CService& service);
387+
static NetInfoStatus ValidateDomainPort(const DomainPort& domain);
387388

388389
private:
389390
uint8_t m_version{CURRENT_VERSION};

src/test/evo_netinfo_tests.cpp

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,12 @@ static const std::vector<TestEntry> addr_vals_main{
4040
// - Non-IPv4 addresses are prohibited in MnNetInfo
4141
// - Any valid BIP155 address is allowed in ExtNetInfo
4242
{{NetInfoPurpose::CORE_P2P, "[2606:4700:4700::1111]:9999"}, NetInfoStatus::BadInput, NetInfoStatus::Success},
43-
// Domains are not allowed
43+
// Domains are not allowed for Core P2P or Platform P2P
4444
{{NetInfoPurpose::CORE_P2P, "example.com:9999"}, NetInfoStatus::BadInput, NetInfoStatus::BadInput},
45+
{{NetInfoPurpose::PLATFORM_P2P, "example.com:9999"}, NetInfoStatus::MaxLimit, NetInfoStatus::BadInput},
46+
// - MnNetInfo doesn't allow storing anything except a Core P2P address
47+
// - ExtNetInfo can store Platform HTTPS addresses *as domains*
48+
{{NetInfoPurpose::PLATFORM_HTTPS, "example.com:9999"}, NetInfoStatus::MaxLimit, NetInfoStatus::Success},
4549
// Incorrect IPv4 address
4650
{{NetInfoPurpose::CORE_P2P, "1.1.1.256:9999"}, NetInfoStatus::BadInput, NetInfoStatus::BadInput},
4751
// Missing address
@@ -182,6 +186,21 @@ BOOST_FIXTURE_TEST_CASE(extnetinfo_rules_reg, RegTestingSetup)
182186
BOOST_CHECK(!netInfo.HasEntries(NetInfoPurpose::PLATFORM_HTTPS));
183187
ValidateGetEntries(netInfo.GetEntries(), /*expected_size=*/2);
184188
}
189+
190+
{
191+
// ExtNetInfo has additional rules for domains
192+
const std::vector<TestEntry> domain_vals{
193+
// Port 80 (HTTP) is below the privileged ports threshold (1023), not allowed
194+
{{NetInfoPurpose::PLATFORM_HTTPS, "example.com:80"}, NetInfoStatus::MaxLimit, NetInfoStatus::BadPort},
195+
// Port 443 (HTTPS) is below the privileged ports threshold (1023) but still allowed
196+
{{NetInfoPurpose::PLATFORM_HTTPS, "example.com:443"}, NetInfoStatus::MaxLimit, NetInfoStatus::Success},
197+
// TLDs must be alphabetic to avoid ambiguation with IP addresses (per ICANN guidelines)
198+
{{NetInfoPurpose::PLATFORM_HTTPS, "example.123:443"}, NetInfoStatus::MaxLimit, NetInfoStatus::BadInput},
199+
// .local is a prohibited TLD
200+
{{NetInfoPurpose::PLATFORM_HTTPS, "somebodys-macbook-pro.local:9998"}, NetInfoStatus::MaxLimit, NetInfoStatus::BadInput},
201+
};
202+
TestExtNetInfo(domain_vals);
203+
}
185204
}
186205

187206
BOOST_AUTO_TEST_CASE(netinfo_ser)
@@ -399,11 +418,15 @@ BOOST_AUTO_TEST_CASE(domainport_rules)
399418

400419
for (const auto& [addr, retval] : domain_vals) {
401420
DomainPort domain;
402-
BOOST_CHECK_EQUAL(domain.Set(addr, 9999), retval);
421+
ExtNetInfo netInfo;
422+
BOOST_CHECK_EQUAL(domain.Set(addr, 443), retval);
403423
if (retval != DomainPort::Status::Success) {
404424
BOOST_CHECK_EQUAL(domain.Validate(), DomainPort::Status::Malformed); // Empty values report as Malformed
425+
BOOST_CHECK_EQUAL(netInfo.AddEntry(NetInfoPurpose::PLATFORM_HTTPS, domain.ToStringAddrPort()),
426+
NetInfoStatus::BadInput);
405427
} else {
406428
BOOST_CHECK_EQUAL(domain.Validate(), DomainPort::Status::Success);
429+
BOOST_CHECK_EQUAL(netInfo.AddEntry(NetInfoPurpose::PLATFORM_HTTPS, domain.ToStringAddrPort()), NetInfoStatus::Success);
407430
}
408431
}
409432

0 commit comments

Comments
 (0)