Use CIDR notation for admin and secure_gateway

This commit is contained in:
Mark Travis
2022-01-26 19:08:25 -08:00
committed by Nik Bougalis
parent dc213a4fab
commit 8f82b62e0d
11 changed files with 241 additions and 83 deletions

View File

@@ -200,9 +200,19 @@
#
# admin = [ IP, IP, IP, ... ]
#
# A comma-separated list of IP addresses.
# A comma-separated list of IP addresses or subnets. Subnets
# should be represented in "slash" notation, such as:
# 10.0.0.0/8
# 172.16.0.0/12
# 192.168.0.0/16
# Those examples are ipv4, but ipv6 is also supported.
# When configuring subnets, the address must match the
# underlying network address. Otherwise, the desired IP range is
# ambiguous. For example, 10.1.2.3/24 has a network address of
# 10.1.2.0. Therefore, that subnet should be configured as
# 10.1.2.0/24.
#
# When set, grants administrative command access to the specified IP
# When set, grants administrative command access to the specified
# addresses. These commands may be issued over http, https, ws, or wss
# if configured on the port. If not provided, the default is to not allow
# administrative commands.
@@ -233,9 +243,10 @@
#
# secure_gateway = [ IP, IP, IP, ... ]
#
# A comma-separated list of IP addresses.
# A comma-separated list of IP addresses or subnets. See the
# details for the "admin" option above.
#
# When set, allows the specified IP addresses to pass HTTP headers
# When set, allows the specified addresses to pass HTTP headers
# containing username and remote IP address for each session. If a
# non-empty username is passed in this way, then resource controls
# such as often resulting in "tooBusy" errors will be lifted. However,
@@ -250,9 +261,9 @@
# proxies. Since rippled trusts these hosts, they must be
# responsible for properly authenticating the remote user.
#
# The same IP address cannot be used in both "admin" and "secure_gateway"
# lists for the same port. In this case, rippled will abort with an error
# message to the console shortly after startup
# If some IP addresses are included for both "admin" and
# "secure_gateway" connections, then they will be treated as
# "admin" addresses.
#
# ssl_key = <filename>
# ssl_cert = <filename>

View File

@@ -25,8 +25,11 @@
#include <ripple/resource/ResourceManager.h>
#include <ripple/server/Handoff.h>
#include <ripple/server/Port.h>
#include <boost/asio/ip/network_v4.hpp>
#include <boost/asio/ip/network_v6.hpp>
#include <boost/utility/string_view.hpp>
#include <string>
#include <variant>
#include <vector>
namespace ripple {
@@ -79,7 +82,8 @@ isUnlimited(Role const& role);
bool
ipAllowed(
beast::IP::Address const& remoteIp,
std::vector<beast::IP::Address> const& adminIp);
std::vector<boost::asio::ip::network_v4> const& nets4,
std::vector<boost::asio::ip::network_v6> const& nets6);
boost::string_view
forwardedFor(http_request_type const& request);

View File

@@ -23,13 +23,14 @@
#include <boost/beast/http/rfc7230.hpp>
#include <boost/utility/string_view.hpp>
#include <algorithm>
#include <tuple>
namespace ripple {
bool
passwordUnrequiredOrSentCorrect(Port const& port, Json::Value const& params)
{
assert(!port.admin_ip.empty());
assert(!(port.admin_nets_v4.empty() && port.admin_nets_v6.empty()));
bool const passwordRequired =
(!port.admin_user.empty() || !port.admin_password.empty());
@@ -43,14 +44,40 @@ passwordUnrequiredOrSentCorrect(Port const& port, Json::Value const& params)
bool
ipAllowed(
beast::IP::Address const& remoteIp,
std::vector<beast::IP::Address> const& adminIp)
std::vector<boost::asio::ip::network_v4> const& nets4,
std::vector<boost::asio::ip::network_v6> const& nets6)
{
return std::find_if(
adminIp.begin(),
adminIp.end(),
[&remoteIp](beast::IP::Address const& ip) {
return ip.is_unspecified() || ip == remoteIp;
}) != adminIp.end();
// To test whether the remoteIP is part of one of the configured
// subnets, first convert it to a subnet definition. For ipv4,
// this means appending /32. For ipv6, /128. Then based on protocol
// check for whether the resulting network is either a subnet of or
// equal to each configured subnet, based on boost::asio's reasoning.
// For example, 10.1.2.3 is a subnet of 10.1.2.0/24, but 10.1.2.0 is
// not. However, 10.1.2.0 is equal to the network portion of 10.1.2.0/24.
std::string addrString = remoteIp.to_string();
if (remoteIp.is_v4())
{
addrString += "/32";
auto ipNet = boost::asio::ip::make_network_v4(addrString);
for (auto const& net : nets4)
{
if (ipNet.is_subnet_of(net) || ipNet == net)
return true;
}
}
else
{
addrString += "/128";
auto ipNet = boost::asio::ip::make_network_v6(addrString);
for (auto const& net : nets6)
{
if (ipNet.is_subnet_of(net) || ipNet == net)
return true;
}
}
return false;
}
bool
@@ -59,7 +86,7 @@ isAdmin(
Json::Value const& params,
beast::IP::Address const& remoteIp)
{
return ipAllowed(remoteIp, port.admin_ip) &&
return ipAllowed(remoteIp, port.admin_nets_v4, port.admin_nets_v6) &&
passwordUnrequiredOrSentCorrect(port, params);
}
@@ -77,7 +104,10 @@ requestRole(
if (required == Role::ADMIN)
return Role::FORBID;
if (ipAllowed(remoteIp.address(), port.secure_gateway_ip))
if (ipAllowed(
remoteIp.address(),
port.secure_gateway_nets_v4,
port.secure_gateway_nets_v6))
{
if (user.size())
return Role::IDENTIFIED;

View File

@@ -1060,10 +1060,6 @@ to_Port(ParsedPort const& parsed, std::ostream& log)
Throw<std::exception>();
}
p.port = *parsed.port;
if (parsed.admin_ip)
p.admin_ip = *parsed.admin_ip;
if (parsed.secure_gateway_ip)
p.secure_gateway_ip = *parsed.secure_gateway_ip;
if (parsed.protocol.empty())
{
@@ -1083,6 +1079,10 @@ to_Port(ParsedPort const& parsed, std::ostream& log)
p.pmd_options = parsed.pmd_options;
p.ws_queue_limit = parsed.ws_queue_limit;
p.limit = parsed.limit;
p.admin_nets_v4 = parsed.admin_nets_v4;
p.admin_nets_v6 = parsed.admin_nets_v6;
p.secure_gateway_nets_v4 = parsed.secure_gateway_nets_v4;
p.secure_gateway_nets_v6 = parsed.secure_gateway_nets_v6;
return p;
}

View File

@@ -45,7 +45,8 @@ public:
if (ipAllowed(
beast::IPAddressConversion::from_asio(ws->remote_endpoint())
.address(),
ws->port().secure_gateway_ip))
ws->port().secure_gateway_nets_v4,
ws->port().secure_gateway_nets_v6))
{
auto it = h.find("X-User");
if (it != h.end())

View File

@@ -23,6 +23,8 @@
#include <ripple/basics/BasicConfig.h>
#include <ripple/beast/net/IPEndpoint.h>
#include <boost/asio/ip/address.hpp>
#include <boost/asio/ip/network_v4.hpp>
#include <boost/asio/ip/network_v6.hpp>
#include <boost/beast/core/string.hpp>
#include <boost/beast/websocket/option.hpp>
#include <cstdint>
@@ -30,6 +32,7 @@
#include <optional>
#include <set>
#include <string>
#include <variant>
namespace boost {
namespace asio {
@@ -50,8 +53,10 @@ struct Port
boost::asio::ip::address ip;
std::uint16_t port = 0;
std::set<std::string, boost::beast::iless> protocol;
std::vector<beast::IP::Address> admin_ip;
std::vector<beast::IP::Address> secure_gateway_ip;
std::vector<boost::asio::ip::network_v4> admin_nets_v4;
std::vector<boost::asio::ip::network_v6> admin_nets_v6;
std::vector<boost::asio::ip::network_v4> secure_gateway_nets_v4;
std::vector<boost::asio::ip::network_v6> secure_gateway_nets_v6;
std::string user;
std::string password;
std::string admin_user;
@@ -108,8 +113,10 @@ struct ParsedPort
std::optional<boost::asio::ip::address> ip;
std::optional<std::uint16_t> port;
std::optional<std::vector<beast::IP::Address>> admin_ip;
std::optional<std::vector<beast::IP::Address>> secure_gateway_ip;
std::vector<boost::asio::ip::network_v4> admin_nets_v4;
std::vector<boost::asio::ip::network_v6> admin_nets_v6;
std::vector<boost::asio::ip::network_v4> secure_gateway_nets_v4;
std::vector<boost::asio::ip::network_v6> secure_gateway_nets_v6;
};
void

View File

@@ -21,8 +21,9 @@
#include <ripple/beast/core/LexicalCast.h>
#include <ripple/beast/rfc2616.h>
#include <ripple/server/Port.h>
#include <boost/algorithm/string/predicate.hpp>
#include <boost/algorithm/string/trim.hpp>
#include <sstream>
namespace ripple {
@@ -47,18 +48,34 @@ operator<<(std::ostream& os, Port const& p)
{
os << "'" << p.name << "' (ip=" << p.ip << ":" << p.port << ", ";
if (!p.admin_ip.empty())
if (p.admin_nets_v4.size() || p.admin_nets_v6.size())
{
os << "admin IPs:";
for (auto const& ip : p.admin_ip)
os << ip.to_string() << ", ";
os << "admin nets:";
for (auto const& net : p.admin_nets_v4)
{
os << net.to_string();
os << ", ";
}
for (auto const& net : p.admin_nets_v6)
{
os << net.to_string();
os << ", ";
}
}
if (!p.secure_gateway_ip.empty())
if (p.secure_gateway_nets_v4.size() || p.secure_gateway_nets_v6.size())
{
os << "secure_gateway IPs:";
for (auto const& ip : p.secure_gateway_ip)
os << ip.to_string() << ", ";
os << "secure_gateway nets:";
for (auto const& net : p.secure_gateway_nets_v4)
{
os << net.to_string();
os << ", ";
}
for (auto const& net : p.secure_gateway_nets_v6)
{
os << net.to_string();
os << ", ";
}
}
os << p.protocols() << ")";
@@ -72,65 +89,108 @@ populate(
Section const& section,
std::string const& field,
std::ostream& log,
std::optional<std::vector<beast::IP::Address>>& ips,
bool allowAllIps,
std::vector<beast::IP::Address> const& admin_ip)
std::vector<boost::asio::ip::network_v4>& nets4,
std::vector<boost::asio::ip::network_v6>& nets6)
{
auto const optResult = section.get(field);
if (optResult)
if (!optResult)
return;
std::stringstream ss(*optResult);
std::string ip;
while (std::getline(ss, ip, ','))
{
std::stringstream ss(*optResult);
std::string ip;
bool has_any(false);
boost::algorithm::trim(ip);
bool v4;
boost::asio::ip::network_v4 v4Net;
boost::asio::ip::network_v6 v6Net;
ips.emplace();
while (std::getline(ss, ip, ','))
try
{
// First, check to see if 0.0.0.0 or ipv6 equivalent was configured,
// which means all IP addresses.
auto const addr = beast::IP::Endpoint::from_string_checked(ip);
if (!addr)
if (addr)
{
log << "Invalid value '" << ip << "' for key '" << field
<< "' in [" << section.name() << "]";
Throw<std::exception>();
}
if (is_unspecified(*addr))
{
if (!allowAllIps)
if (is_unspecified(*addr))
{
log << addr->address() << " not allowed'"
<< "' for key '" << field << "' in [" << section.name()
<< "]";
Throw<std::exception>();
nets4.push_back(
boost::asio::ip::make_network_v4("0.0.0.0/0"));
nets6.push_back(boost::asio::ip::make_network_v6("::/0"));
// No reason to allow more IPs--it would be redundant.
break;
}
// The configured address is a single IP (or else addr would
// be unset). We need this to be a subnet, so append
// the number of network bits to make a subnet of 1,
// depending on type.
v4 = addr->is_v4();
std::string addressString = addr->to_string();
if (v4)
{
addressString += "/32";
v4Net = boost::asio::ip::make_network_v4(addressString);
}
else
{
has_any = true;
addressString += "/128";
v6Net = boost::asio::ip::make_network_v6(addressString);
}
}
else
{
// Since addr is empty, assume that the entry is
// for a subnet which includes trailing /0-32 or /0-128
// depending on ip type.
// First, see if it's an ipv4 subnet. If not, try ipv6.
// If that throws, then there's nothing we can do with
// the entry.
try
{
v4Net = boost::asio::ip::make_network_v4(ip);
v4 = true;
}
catch (boost::system::system_error const& e)
{
v6Net = boost::asio::ip::make_network_v6(ip);
v4 = false;
}
}
if (has_any && !ips->empty())
// Confirm that the address entry is the same as the subnet's
// underlying network address.
// 10.1.2.3/24 makes no sense. The underlying network address
// is 10.1.2.0/24.
if (v4)
{
log << "IP specified along with " << addr->address() << " '"
<< ip << "' for key '" << field << "' in ["
<< section.name() << "]";
Throw<std::exception>();
if (v4Net != v4Net.canonical())
{
log << "The configured subnet " << v4Net.to_string()
<< " is not the same as the network address, which is "
<< v4Net.canonical().to_string();
Throw<std::exception>();
}
nets4.push_back(v4Net);
}
auto const& address = addr->address();
if (std::find_if(
admin_ip.begin(),
admin_ip.end(),
[&address](beast::IP::Address const& a) {
return address == a;
}) != admin_ip.end())
else
{
log << "IP specified for " << field << " is also for "
<< "admin: " << ip << " in [" << section.name() << "]";
Throw<std::exception>();
if (v6Net != v6Net.canonical())
{
log << "The configured subnet " << v6Net.to_string()
<< " is not the same as the network address, which is "
<< v6Net.canonical().to_string();
Throw<std::exception>();
}
nets6.push_back(v6Net);
}
ips->emplace_back(addr->address());
}
catch (boost::system::system_error const& e)
{
log << "Invalid value '" << ip << "' for key '" << field << "' in ["
<< section.name() << "]: " << e.what();
Throw<std::exception>();
}
}
}
@@ -232,14 +292,13 @@ parse_Port(ParsedPort& port, Section const& section, std::ostream& log)
}
}
populate(section, "admin", log, port.admin_ip, true, {});
populate(section, "admin", log, port.admin_nets_v4, port.admin_nets_v6);
populate(
section,
"secure_gateway",
log,
port.secure_gateway_ip,
false,
port.admin_ip.value_or(std::vector<beast::IP::Address>{}));
port.secure_gateway_nets_v4,
port.secure_gateway_nets_v6);
set(port.user, "user", section);
set(port.password, "password", section);

View File

@@ -810,11 +810,11 @@ trustthesevalidators.gov
ParsedPort rpc;
if (!unexcept([&]() { parse_Port(rpc, conf["port_rpc"], log); }))
return;
BEAST_EXPECT(rpc.admin_ip && (rpc.admin_ip.value().size() == 2));
BEAST_EXPECT(rpc.admin_nets_v4.size() + rpc.admin_nets_v6.size() == 2);
ParsedPort wss;
if (!unexcept([&]() { parse_Port(wss, conf["port_wss_admin"], log); }))
return;
BEAST_EXPECT(wss.admin_ip && (wss.admin_ip.value().size() == 1));
BEAST_EXPECT(wss.admin_nets_v4.size() + wss.admin_nets_v6.size() == 1);
}
void

View File

@@ -84,6 +84,10 @@ std::unique_ptr<Config> no_admin(std::unique_ptr<Config>);
std::unique_ptr<Config> secure_gateway(std::unique_ptr<Config>);
std::unique_ptr<Config> admin_localnet(std::unique_ptr<Config>);
std::unique_ptr<Config> secure_gateway_localnet(std::unique_ptr<Config>);
/// @brief adjust configuration with params needed to be a validator
///
/// this is intended for use with envconfig, as in

View File

@@ -83,6 +83,24 @@ secure_gateway(std::unique_ptr<Config> cfg)
return cfg;
}
std::unique_ptr<Config>
admin_localnet(std::unique_ptr<Config> cfg)
{
(*cfg)["port_rpc"].set("admin", "127.0.0.0/8");
(*cfg)["port_ws"].set("admin", "127.0.0.0/8");
return cfg;
}
std::unique_ptr<Config>
secure_gateway_localnet(std::unique_ptr<Config> cfg)
{
(*cfg)["port_rpc"].set("admin", "");
(*cfg)["port_ws"].set("admin", "");
(*cfg)["port_rpc"].set("secure_gateway", "127.0.0.0/8");
(*cfg)["port_ws"].set("secure_gateway", "127.0.0.0/8");
return cfg;
}
auto constexpr defaultseed = "shUwVw52ofnCUX5m7kPTKzJdr4HEH";
std::unique_ptr<Config>

View File

@@ -269,6 +269,30 @@ class Roles_test : public beast::unit_test::suite
BEAST_EXPECT(rpcRes["ip"] == "::11:22:33:44:45.55.65.75");
BEAST_EXPECT(isValidIpAddress(rpcRes["ip"].asString()));
}
{
Env env{*this, envconfig(admin_localnet)};
BEAST_EXPECT(env.rpc("ping")["result"]["role"] == "admin");
BEAST_EXPECT(makeWSClient(env.app().config())
->invoke("ping")["result"]["unlimited"]
.asBool());
}
{
Env env{*this, envconfig(secure_gateway_localnet)};
BEAST_EXPECT(env.rpc("ping")["result"]["role"] == "proxied");
auto wsRes =
makeWSClient(env.app().config())->invoke("ping")["result"];
BEAST_EXPECT(
!wsRes.isMember("unlimited") || !wsRes["unlimited"].asBool());
std::unordered_map<std::string, std::string> headers;
headers["X-Forwarded-For"] = "12.34.56.78";
Json::Value rpcRes = env.rpc(headers, "ping")["result"];
BEAST_EXPECT(rpcRes["role"] == "proxied");
BEAST_EXPECT(rpcRes["ip"] == "12.34.56.78");
BEAST_EXPECT(isValidIpAddress(rpcRes["ip"].asString()));
}
}
void