net/i2p: support human-readable hostnames

This commit is contained in:
jpk68
2026-07-28 14:27:51 -04:00
parent 5c9619432b
commit 3a63ea3001
3 changed files with 220 additions and 21 deletions
+56 -10
View File
@@ -45,21 +45,16 @@ namespace net
{
namespace
{
// !TODO only b32 addresses right now
constexpr const char tld[] = u8".b32.i2p";
constexpr const char unknown_host[] = "<unknown i2p host>";
constexpr const unsigned b32_length = 52;
constexpr const char base32_alphabet[] =
u8"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz234567";
expect<void> host_check(boost::string_ref host) noexcept
//! Validate a Base32 address (.b32.i2p)
expect<void> host_check_b32(boost::string_ref host) noexcept
{
if (!host.ends_with(tld))
if (!host.ends_with(tld_b32))
return {net::error::expected_tld};
host.remove_suffix(sizeof(tld) - 1);
host.remove_suffix(sizeof(tld_b32) - 1);
if (host.size() != b32_length)
return {net::error::invalid_i2p_address};
@@ -69,6 +64,55 @@ namespace net
return success();
}
//! Validate a human-readable hostname (.i2p)
expect<void> host_check_human(boost::string_ref host) noexcept
{
if (!host.ends_with(tld_i2p))
return {net::error::expected_tld};
host.remove_suffix(sizeof(tld_i2p) - 1);
if (host.empty() || host.size() > i2p_name_max_length)
return {net::error::invalid_i2p_address};
//! Reject addresses starting or ending with a dot or hyphen
if (host.front() == '.' || host.front() == '-' ||
host.back() == '.' || host.back() == '-')
return {net::error::invalid_i2p_address};
//! Reject invalid characters, and two dots/hyphens in a row (or a dot next to a hyphen)
char previous = '\0';
for (const char c : host)
{
if (!(('a' <= c && c <= 'z') || ('0' <= c && c <= '9') || c == '.' || c == '-'))
{
return {net::error::invalid_i2p_address};
}
if ((c == '.' || c == '-') && (previous == '.' || previous == '-'))
{
return {net::error::invalid_i2p_address};
}
previous = c;
}
return success();
}
//! Use appropriate function for address validation
expect<void> host_check(boost::string_ref host) noexcept
{
if (host.ends_with(tld_b32))
{
return host_check_b32(host);
}
else if (host.ends_with(tld_i2p))
{
return host_check_human(host);
}
return {net::error::expected_tld};
}
struct i2p_serialized
{
std::string host;
@@ -110,7 +154,9 @@ namespace net
net::canonicalize_host(normalized_host);
MONERO_CHECK(host_check(normalized_host));
static_assert(b32_length + sizeof(tld) == sizeof(i2p_address::host_), "bad internal host size");
static_assert(sizeof(i2p_address::host_) >= b32_length + sizeof(tld_b32) &&
sizeof(i2p_address::host_) >= i2p_name_max_length + sizeof(tld_i2p));
return i2p_address{normalized_host};
}
+23 -6
View File
@@ -47,10 +47,27 @@ namespace serialization
namespace net
{
//! b32 i2p address; internal format not condensed/decoded.
/**
* Naming rules defined here:
* https://i2p.net/en/docs/overview/naming/#naming-rules
*/
constexpr const char tld_b32[] = u8".b32.i2p";
constexpr const char tld_i2p[] = u8".i2p";
constexpr const char unknown_host[] = "<unknown i2p host>";
//! Length of a Base32 I2P address in characters, not including TLD
constexpr const unsigned b32_length = 52;
//! Maximum length of a human-readable I2P hostname in characters, not including TLD
constexpr const unsigned i2p_name_max_length = 63;
/**
* An I2P address; internal format not condensed/decoded.
* Supports human-readable and Base32 types.
*/
class i2p_address
{
char host_[61]; // null-terminated
char host_[i2p_name_max_length + sizeof(tld_i2p)] = {}; // null-terminated
//! Keep in private, `host.size()` has no runtime check
i2p_address(boost::string_ref host) noexcept;
@@ -59,7 +76,7 @@ namespace net
//! \return Size of internal buffer for host.
static constexpr std::size_t buffer_size() noexcept { return sizeof(host_); }
//! \return `<unknown tor host>`.
//! \return `<unknown i2p host>`.
static const char* unknown_str() noexcept;
//! An object with `port() == 0` and `host_str() == unknown_str()`.
@@ -69,7 +86,7 @@ namespace net
static i2p_address unknown() noexcept { return i2p_address{}; }
/*!
Parse `address` in b32 i2p format (i.e. x.b32.i2p:80)
Parse `address` in the detected format (i.e. x[.b32].i2p:80)
with `default_port` being used if port is not specified in
`address`.
*/
@@ -96,10 +113,10 @@ namespace net
//! \return True if i2p addresses are identical.
bool is_same_host(const i2p_address& rhs) const noexcept;
//! \return `x.b32.i2p` or `x.b32.i2p:z` if `port() != 0`.
//! \return `x[.b32].i2p` or `x[.b32].i2p:z` if `port() != 0`.
std::string str() const;
//! \return Null-terminated `x.b32.i2p` value or `unknown_str()`.
//! \return Null-terminated `x[.b32].i2p` value or `unknown_str()`.
const char* host_str() const noexcept { return host_; }
//! \return `1` to work with I2P socks which considers `0` error.
+141 -5
View File
@@ -1,5 +1,4 @@
// Copyright (c) 2018-2024, The Monero Project
//
// All rights reserved.
//
@@ -491,6 +490,9 @@ namespace
"VWW6YBAL4BD7SZMGNCYRUUCPGFKQAHZDDI37KTCEO3AH7NGMCOPN.B32.I2P";
static constexpr const char b32_i2p_2[] =
"xmrto2bturnore26xmrto2bturnore26xmrto2bturnore26xmr2.b32.i2p";
static constexpr const char standard_i2p[] = "test.i2p";
static constexpr const char standard_i2p_2[] = "reg.i2p";
}
TEST(i2p_address, constants)
@@ -511,12 +513,40 @@ TEST(i2p_address, invalid)
EXPECT_TRUE(net::i2p_address::make(":").has_error());
EXPECT_TRUE(net::i2p_address::make(".b32.i2p").has_error());
EXPECT_TRUE(net::i2p_address::make(".b32.i2p:").has_error());
EXPECT_TRUE(net::i2p_address::make(".i2p").has_error());
EXPECT_TRUE(net::i2p_address::make(".i2p:").has_error());
EXPECT_TRUE(net::i2p_address::make("m=nero.i2p").has_error());
EXPECT_TRUE(net::i2p_address::make("-monero.i2p").has_error());
EXPECT_TRUE(net::i2p_address::make(std::string(net::i2p_name_max_length, 'a') + ".i2p").has_value());
EXPECT_TRUE(net::i2p_address::make(std::string(net::i2p_name_max_length + 1, 'a') + ".i2p").has_error());
EXPECT_TRUE(net::i2p_address::make("monero-.i2p").has_error());
EXPECT_TRUE(net::i2p_address::make("a.-monero.i2p").has_error());
EXPECT_TRUE(net::i2p_address::make("m..ero.i2p").has_error());
EXPECT_TRUE(net::i2p_address::make("m..ero.b32.i2p.i2p").has_error());
EXPECT_TRUE(net::i2p_address::make("monero.i2p-").has_error());
EXPECT_TRUE(net::i2p_address::make("monero..i2p").has_error());
EXPECT_TRUE(net::i2p_address::make("a-.b.i2p").has_error());
EXPECT_TRUE(net::i2p_address::make("a.-b.i2p").has_error());
EXPECT_TRUE(net::i2p_address::make("a--b.i2p").has_error());
EXPECT_TRUE(net::i2p_address::make("a.b.i2p").has_value());
EXPECT_TRUE(net::i2p_address::make("abc-123.i2p").has_value());
EXPECT_TRUE(net::i2p_address::make("example.b32.i2p").has_error());
EXPECT_TRUE(net::i2p_address::make(b32_i2p + 1).has_error());
EXPECT_TRUE(net::i2p_address::make("test.b32.i2p.i2p").has_value());
EXPECT_TRUE(net::i2p_address::make(std::string{b32_i2p} + ".i2p").has_value());
EXPECT_TRUE(net::i2p_address::make(boost::string_ref{b32_i2p, sizeof(b32_i2p) - 2}).has_error());
EXPECT_TRUE(net::i2p_address::make(boost::string_ref{standard_i2p, sizeof(standard_i2p) - 2}).has_error());
std::string i2p{b32_i2p};
i2p.at(10) = 1;
EXPECT_TRUE(net::i2p_address::make(i2p).has_error());
std::string i2p_standard{standard_i2p};
i2p_standard.at(7) = 1;
EXPECT_TRUE(net::i2p_address::make(i2p_standard).has_error());
}
TEST(i2p_address, unblockable_types)
@@ -547,7 +577,7 @@ TEST(i2p_address, unblockable_types)
EXPECT_EQ(net::i2p_address{}, net::i2p_address::unknown());
}
TEST(i2p_address, valid)
TEST(i2p_address, valid_b32)
{
const auto address1 = net::i2p_address::make(b32_i2p);
@@ -624,6 +654,85 @@ TEST(i2p_address, valid)
EXPECT_FALSE(address2.less(address3));
}
TEST(i2p_address, valid_standard)
{
const auto address1 = net::i2p_address::make(standard_i2p);
ASSERT_TRUE(address1.has_value());
EXPECT_EQ(1u, address1->port());
EXPECT_STREQ(standard_i2p, address1->host_str());
EXPECT_STREQ(standard_i2p, address1->str().c_str());
EXPECT_TRUE(address1->is_blockable());
net::i2p_address address2{*address1};
EXPECT_EQ(1u, address2.port());
EXPECT_STREQ(standard_i2p, address2.host_str());
EXPECT_STREQ(standard_i2p, address2.str().c_str());
EXPECT_TRUE(address2.is_blockable());
EXPECT_TRUE(address2.equal(*address1));
EXPECT_TRUE(address1->equal(address2));
EXPECT_TRUE(address2 == *address1);
EXPECT_TRUE(*address1 == address2);
EXPECT_FALSE(address2 != *address1);
EXPECT_FALSE(*address1 != address2);
EXPECT_TRUE(address2.is_same_host(*address1));
EXPECT_TRUE(address1->is_same_host(address2));
EXPECT_FALSE(address2.less(*address1));
EXPECT_FALSE(address1->less(address2));
address2 = MONERO_UNWRAP(net::i2p_address::make(std::string{standard_i2p_2} + ":6545"));
EXPECT_EQ(1u, address2.port());
EXPECT_STREQ(standard_i2p_2, address2.host_str());
EXPECT_EQ(std::string{standard_i2p_2}, address2.str().c_str());
EXPECT_TRUE(address2.is_blockable());
EXPECT_FALSE(address2.equal(*address1));
EXPECT_FALSE(address1->equal(address2));
EXPECT_FALSE(address2 == *address1);
EXPECT_FALSE(*address1 == address2);
EXPECT_TRUE(address2 != *address1);
EXPECT_TRUE(*address1 != address2);
EXPECT_FALSE(address2.is_same_host(*address1));
EXPECT_FALSE(address1->is_same_host(address2));
EXPECT_FALSE(address1->less(address2));
EXPECT_TRUE(address2.less(*address1));
net::i2p_address address3 = MONERO_UNWRAP(net::i2p_address::make(std::string{standard_i2p} + ":65535"));
EXPECT_EQ(1u, address3.port());
EXPECT_STREQ(standard_i2p, address3.host_str());
EXPECT_EQ(std::string{standard_i2p}, address3.str().c_str());
EXPECT_TRUE(address3.is_blockable());
EXPECT_TRUE(address3.equal(*address1));
EXPECT_TRUE(address1->equal(address3));
EXPECT_TRUE(address3 == *address1);
EXPECT_TRUE(*address1 == address3);
EXPECT_FALSE(address3 != *address1);
EXPECT_FALSE(*address1 != address3);
EXPECT_TRUE(address3.is_same_host(*address1));
EXPECT_TRUE(address1->is_same_host(address3));
EXPECT_FALSE(address3.less(*address1));
EXPECT_FALSE(address1->less(address3));
EXPECT_FALSE(address3.equal(address2));
EXPECT_FALSE(address2.equal(address3));
EXPECT_FALSE(address3 == address2);
EXPECT_FALSE(address2 == address3);
EXPECT_TRUE(address3 != address2);
EXPECT_TRUE(address2 != address3);
EXPECT_FALSE(address3.is_same_host(address2));
EXPECT_FALSE(address2.is_same_host(address3));
EXPECT_TRUE(address2.less(address3));
EXPECT_FALSE(address3.less(address2));
{
const auto address = net::i2p_address::make("Example.I2P");
ASSERT_TRUE(address.has_value());
EXPECT_STREQ("example.i2p", address->host_str());
}
}
TEST(i2p_address, generic_network_address)
{
const epee::net_utils::network_address i2p1{MONERO_UNWRAP(net::i2p_address::make(b32_i2p))};
@@ -645,6 +754,22 @@ TEST(i2p_address, generic_network_address)
EXPECT_TRUE(i2p1.is_blockable());
EXPECT_TRUE(i2p2.is_blockable());
EXPECT_TRUE(ip.is_blockable());
const epee::net_utils::network_address i2p1_standard{MONERO_UNWRAP(net::i2p_address::make(standard_i2p))};
const epee::net_utils::network_address i2p2_standard{MONERO_UNWRAP(net::i2p_address::make(standard_i2p))};
EXPECT_EQ(i2p1_standard, i2p2_standard);
EXPECT_NE(ip, i2p1_standard);
EXPECT_LT(ip, i2p1_standard);
EXPECT_STREQ(standard_i2p, i2p1_standard.host_str().c_str());
EXPECT_STREQ(standard_i2p, i2p1_standard.str().c_str());
EXPECT_EQ(epee::net_utils::address_type::i2p, i2p1_standard.get_type_id());
EXPECT_EQ(epee::net_utils::address_type::i2p, i2p2_standard.get_type_id());
EXPECT_EQ(epee::net_utils::zone::i2p, i2p1_standard.get_zone());
EXPECT_EQ(epee::net_utils::zone::i2p, i2p2_standard.get_zone());
EXPECT_TRUE(i2p1_standard.is_blockable());
EXPECT_TRUE(i2p2_standard.is_blockable());
}
namespace
@@ -847,9 +972,8 @@ TEST(i2p_address, boost_serialize_unknown)
TEST(get_network_address, i2p)
{
expect<epee::net_utils::network_address> address =
net::get_network_address("i2p", 0);
EXPECT_EQ(net::error::unsupported_address, address);
expect<epee::net_utils::network_address> address = net::get_network_address(".i2p", 0);
EXPECT_EQ(net::error::invalid_i2p_address, address);
address = net::get_network_address(".b32.i2p", 0);
EXPECT_EQ(net::error::invalid_i2p_address, address);
@@ -865,6 +989,18 @@ TEST(get_network_address, i2p)
EXPECT_EQ(epee::net_utils::address_type::i2p, address->get_type_id());
EXPECT_STREQ(b32_i2p, address->host_str().c_str());
EXPECT_EQ(std::string{b32_i2p}, address->str());
address = net::get_network_address(standard_i2p, 1000);
ASSERT_TRUE(bool(address));
EXPECT_EQ(epee::net_utils::address_type::i2p, address->get_type_id());
EXPECT_STREQ(standard_i2p, address->host_str().c_str());
EXPECT_EQ(std::string{standard_i2p}, address->str());
address = net::get_network_address(std::string{standard_i2p} + ":2000", 1000);
ASSERT_TRUE(bool(address));
EXPECT_EQ(epee::net_utils::address_type::i2p, address->get_type_id());
EXPECT_STREQ(standard_i2p, address->host_str().c_str());
EXPECT_EQ(std::string{standard_i2p}, address->str());
}
TEST(get_network_address, ipv4)