Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix checking if a client host is allowed. #8241

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
152 changes: 96 additions & 56 deletions dbms/src/Access/AllowedClientHosts.cpp
Expand Up @@ -9,6 +9,7 @@
#include <ext/scope_guard.h>
#include <boost/range/algorithm/find.hpp>
#include <boost/range/algorithm/find_first_of.hpp>
#include <ifaddrs.h>


namespace DB
Expand All @@ -30,10 +31,12 @@ namespace
if (addr.family() == IPAddress::IPv6)
return addr;

if (addr.isLoopback())
return IPAddress("::1");

return IPAddress("::FFFF:" + addr.toString());
}


IPAddress maskToIPv6(const IPAddress & mask)
{
if (mask.family() == IPAddress::IPv6)
Expand All @@ -48,38 +51,38 @@ namespace
IPAddress addr_v6 = toIPv6(address);

/// Resolve by hand, because Poco don't use AI_ALL flag but we need it.
addrinfo * ai = nullptr;
addrinfo * ai_begin = nullptr;
SCOPE_EXIT(
{
if (ai)
freeaddrinfo(ai);
if (ai_begin)
freeaddrinfo(ai_begin);
});

addrinfo hints;
memset(&hints, 0, sizeof(hints));
hints.ai_family = AF_UNSPEC;
hints.ai_flags |= AI_V4MAPPED | AI_ALL;

int ret = getaddrinfo(host.c_str(), nullptr, &hints, &ai);
if (0 != ret)
throw Exception("Cannot getaddrinfo: " + std::string(gai_strerror(ret)), ErrorCodes::DNS_ERROR);
int err = getaddrinfo(host.c_str(), nullptr, &hints, &ai_begin);
if (err)
throw Exception("Cannot getaddrinfo(" + host + "): " + gai_strerror(err), ErrorCodes::DNS_ERROR);

for (; ai != nullptr; ai = ai->ai_next)
for (const addrinfo * ai = ai_begin; ai; ai = ai->ai_next)
{
if (ai->ai_addrlen && ai->ai_addr)
{
if (ai->ai_family == AF_INET6)
if (ai->ai_family == AF_INET)
{
if (addr_v6 == IPAddress(
&reinterpret_cast<sockaddr_in6*>(ai->ai_addr)->sin6_addr, sizeof(in6_addr),
reinterpret_cast<sockaddr_in6*>(ai->ai_addr)->sin6_scope_id))
const auto & sin = *reinterpret_cast<const sockaddr_in *>(ai->ai_addr);
if (addr_v6 == toIPv6(IPAddress(&sin.sin_addr, sizeof(sin.sin_addr))))
{
return true;
}
}
else if (ai->ai_family == AF_INET)
else if (ai->ai_family == AF_INET6)
{
if (addr_v6 == toIPv6(IPAddress(&reinterpret_cast<sockaddr_in *>(ai->ai_addr)->sin_addr, sizeof(in_addr))))
const auto & sin = *reinterpret_cast<const sockaddr_in6*>(ai->ai_addr);
if (addr_v6 == IPAddress(&sin.sin6_addr, sizeof(sin.sin6_addr), sin.sin6_scope_id))
{
return true;
}
Expand All @@ -99,19 +102,61 @@ namespace
}


std::vector<IPAddress> getAddressesOfLocalhostImpl()
{
std::vector<IPAddress> addresses;

ifaddrs * ifa_begin = nullptr;
SCOPE_EXIT({
if (ifa_begin)
freeifaddrs(ifa_begin);
});

int err = getifaddrs(&ifa_begin);
if (err)
return {IPAddress{"127.0.0.1"}, IPAddress{"::1"}};

for (const ifaddrs * ifa = ifa_begin; ifa; ifa = ifa->ifa_next)
{
if (!ifa->ifa_addr)
continue;
if (ifa->ifa_addr->sa_family == AF_INET)
{
const auto & sin = *reinterpret_cast<const sockaddr_in *>(ifa->ifa_addr);
addresses.push_back(toIPv6(IPAddress(&sin.sin_addr, sizeof(sin.sin_addr))));
}
else if (ifa->ifa_addr->sa_family == AF_INET6)
{
const auto & sin = *reinterpret_cast<const sockaddr_in6 *>(ifa->ifa_addr);
addresses.push_back(IPAddress(&sin.sin6_addr, sizeof(sin.sin6_addr), sin.sin6_scope_id));
}
}
return addresses;
}


/// Checks if a specified address pointers to the localhost.
bool isLocalAddress(const IPAddress & address)
{
static const std::vector<IPAddress> local_addresses = getAddressesOfLocalhostImpl();
return boost::range::find(local_addresses, address) != local_addresses.end();
}


String getHostByAddressImpl(const IPAddress & address)
{
Poco::Net::SocketAddress sock_addr(address, 0);

/// Resolve by hand, because Poco library doesn't have such functionality.
char host[1024];
int gai_errno = getnameinfo(sock_addr.addr(), sock_addr.length(), host, sizeof(host), nullptr, 0, NI_NAMEREQD);
if (0 != gai_errno)
throw Exception("Cannot getnameinfo: " + std::string(gai_strerror(gai_errno)), ErrorCodes::DNS_ERROR);
int err = getnameinfo(sock_addr.addr(), sock_addr.length(), host, sizeof(host), nullptr, 0, NI_NAMEREQD);
if (err)
throw Exception("Cannot getnameinfo(" + address.toString() + "): " + gai_strerror(err), ErrorCodes::DNS_ERROR);

/// Check that PTR record is resolved back to client address
if (!isAddressOfHost(address, host))
throw Exception("Host " + String(host) + " isn't resolved back to " + address.toString(), ErrorCodes::DNS_ERROR);

return host;
}

Expand Down Expand Up @@ -158,6 +203,7 @@ AllowedClientHosts::AllowedClientHosts(const AllowedClientHosts & src)
AllowedClientHosts & AllowedClientHosts::operator =(const AllowedClientHosts & src)
{
addresses = src.addresses;
loopback = src.loopback;
subnets = src.subnets;
host_names = src.host_names;
host_regexps = src.host_regexps;
Expand All @@ -175,6 +221,7 @@ AllowedClientHosts::AllowedClientHosts(AllowedClientHosts && src)
AllowedClientHosts & AllowedClientHosts::operator =(AllowedClientHosts && src)
{
addresses = std::move(src.addresses);
loopback = src.loopback;
subnets = std::move(src.subnets);
host_names = std::move(src.host_names);
host_regexps = std::move(src.host_regexps);
Expand All @@ -186,6 +233,7 @@ AllowedClientHosts & AllowedClientHosts::operator =(AllowedClientHosts && src)
void AllowedClientHosts::clear()
{
addresses.clear();
loopback = false;
subnets.clear();
host_names.clear();
host_regexps.clear();
Expand All @@ -204,6 +252,8 @@ void AllowedClientHosts::addAddress(const IPAddress & address)
IPAddress addr_v6 = toIPv6(address);
if (boost::range::find(addresses, addr_v6) == addresses.end())
addresses.push_back(addr_v6);
if (addr_v6.isLoopback())
loopback = true;
}


Expand Down Expand Up @@ -291,30 +341,28 @@ bool AllowedClientHosts::containsAllAddresses() const
}


bool AllowedClientHosts::contains(const IPAddress & address) const
{
return containsImpl(address, String(), nullptr);
}


void AllowedClientHosts::checkContains(const IPAddress & address, const String & user_name) const
{
String error;
if (!containsImpl(address, user_name, &error))
throw Exception(error, ErrorCodes::IP_ADDRESS_NOT_ALLOWED);
if (!contains(address))
{
if (user_name.empty())
throw Exception("It's not allowed to connect from address " + address.toString(), ErrorCodes::IP_ADDRESS_NOT_ALLOWED);
else
throw Exception("User " + user_name + " is not allowed to connect from address " + address.toString(), ErrorCodes::IP_ADDRESS_NOT_ALLOWED);
}
}


bool AllowedClientHosts::containsImpl(const IPAddress & address, const String & user_name, String * error) const
bool AllowedClientHosts::contains(const IPAddress & address) const
{
if (error)
error->clear();

/// Check `ip_addresses`.
IPAddress addr_v6 = toIPv6(address);
if (boost::range::find(addresses, addr_v6) != addresses.end())
return true;

if (loopback && isLocalAddress(addr_v6))
return true;

/// Check `ip_subnets`.
for (const auto & subnet : subnets)
if ((addr_v6 & subnet.mask) == subnet.prefix)
Expand All @@ -325,14 +373,13 @@ bool AllowedClientHosts::containsImpl(const IPAddress & address, const String &
{
try
{
if (isAddressOfHost(address, host_name))
if (isAddressOfHost(addr_v6, host_name))
return true;
}
catch (Exception & e)
catch (const Exception & e)
{
if (e.code() != ErrorCodes::DNS_ERROR)
e.rethrow();

throw;
/// Try to ignore DNS errors: if host cannot be resolved, skip it and try next.
LOG_WARNING(
&Logger::get("AddressPatterns"),
Expand All @@ -342,38 +389,31 @@ bool AllowedClientHosts::containsImpl(const IPAddress & address, const String &
}

/// Check `host_regexps`.
if (!host_regexps.empty())
try
{
compileRegexps();
try
String resolved_host = getHostByAddress(addr_v6);
if (!resolved_host.empty())
{
String resolved_host = getHostByAddress(address);
compileRegexps();
for (const auto & compiled_regexp : compiled_host_regexps)
{
if (compiled_regexp && compiled_regexp->match(resolved_host))
Poco::RegularExpression::Match match;
if (compiled_regexp && compiled_regexp->match(resolved_host, match))
return true;
}
}
catch (Exception & e)
{
if (e.code() != ErrorCodes::DNS_ERROR)
e.rethrow();

/// Try to ignore DNS errors: if host cannot be resolved, skip it and try next.
LOG_WARNING(
&Logger::get("AddressPatterns"),
"Failed to check if the allowed client hosts contain address " << address.toString() << ". " << e.displayText()
<< ", code = " << e.code());
}
}

if (error)
catch (const Exception & e)
{
if (user_name.empty())
*error = "It's not allowed to connect from address " + address.toString();
else
*error = "User " + user_name + " is not allowed to connect from address " + address.toString();
if (e.code() != ErrorCodes::DNS_ERROR)
throw;
/// Try to ignore DNS errors: if host cannot be resolved, skip it and try next.
LOG_WARNING(
&Logger::get("AddressPatterns"),
"Failed to check if the allowed client hosts contain address " << address.toString() << ". " << e.displayText()
<< ", code = " << e.code());
}

return false;
}

Expand Down
2 changes: 1 addition & 1 deletion dbms/src/Access/AllowedClientHosts.h
Expand Up @@ -91,10 +91,10 @@ class AllowedClientHosts
friend bool operator !=(const AllowedClientHosts & lhs, const AllowedClientHosts & rhs) { return !(lhs == rhs); }

private:
bool containsImpl(const IPAddress & address, const String & user_name, String * error) const;
void compileRegexps() const;

std::vector<IPAddress> addresses;
bool loopback = false;
std::vector<IPSubnet> subnets;
std::vector<String> host_names;
std::vector<String> host_regexps;
Expand Down
14 changes: 9 additions & 5 deletions dbms/tests/integration/helpers/cluster.py
Expand Up @@ -534,6 +534,8 @@ def add_zookeeper_startup_command(self, command):
{app_net}
{ipv4_address}
{ipv6_address}
{net_aliases}
{net_alias1}
'''


Expand Down Expand Up @@ -883,17 +885,17 @@ def create_dir(self, destroy_dir=True):
if self.stay_alive:
entrypoint_cmd = CLICKHOUSE_STAY_ALIVE_COMMAND

ipv4_address = ipv6_address = ""
if self.ipv4_address is None and self.ipv6_address is None:
networks = ""
app_net = ""
else:
networks = app_net = ipv4_address = ipv6_address = net_aliases = net_alias1 = ""
if self.ipv4_address is not None or self.ipv6_address is not None or self.hostname != self.name:
networks = "networks:"
app_net = "default:"
if self.ipv4_address is not None:
ipv4_address = "ipv4_address: " + self.ipv4_address
if self.ipv6_address is not None:
ipv6_address = "ipv6_address: " + self.ipv6_address
if self.hostname != self.name:
net_aliases = "aliases:"
net_alias1 = "- " + self.hostname

if not self.with_installed_binary:
binary_volume = "- " + self.server_bin_path + ":/usr/bin/clickhouse"
Expand Down Expand Up @@ -923,6 +925,8 @@ def create_dir(self, destroy_dir=True):
app_net=app_net,
ipv4_address=ipv4_address,
ipv6_address=ipv6_address,
net_aliases = net_aliases,
net_alias1 = net_alias1,
))

def destroy_dir(self):
Expand Down
Empty file.
@@ -0,0 +1,35 @@
<?xml version="1.0"?>
<yandex>
<users>
<default>
<!-- List of networks with open access.

To open access from everywhere, specify:
<ip>::/0</ip>

To open access only from localhost, specify:
<ip>::1</ip>
<ip>127.0.0.1</ip>

Each element of list has one of the following forms:
<ip> IP-address or network mask. Examples: 213.180.204.3 or 10.0.0.1/8 or 10.0.0.1/255.255.255.0
2a02:6b8::3 or 2a02:6b8::3/64 or 2a02:6b8::3/ffff:ffff:ffff:ffff::.
<host> Hostname. Example: server01.yandex.ru.
To check access, DNS query is performed, and all received addresses compared to peer address.
<host_regexp> Regular expression for host names. Example, ^server\d\d-\d\d-\d\.yandex\.ru$
To check access, DNS PTR query is performed for peer address and then regexp is applied.
Then, for result of PTR query, another DNS query is performed and all received addresses compared to peer address.
Strongly recommended that regexp is ends with $
All results of DNS requests are cached till server restart.
-->
<networks>
<ip>127.0.0.1</ip>
<host>clientA1.com</host>
<host>clientA3.com</host>
<host_regexp>clientB\d+\.ru</host_regexp>
<host_regexp>clientC\d+\.ru$</host_regexp>
<host_regexp>^clientD\d+\.ru$</host_regexp>
</networks>
</default>
</users>
</yandex>
13 changes: 13 additions & 0 deletions dbms/tests/integration/test_allowed_client_hosts/configs/users.xml
@@ -0,0 +1,13 @@
<?xml version="1.0"?>
<yandex>
<profiles>
<default>
</default>
</profiles>
<users>
<default>
<profile>default</profile>
<password></password>
</default>
</users>
</yandex>