fix: Client IP resolving when proxy reusing TCP connection (#3043)

Proxy may reuse TCP connections to send HTTP requests from different
clients. This PR fixes bug when Clio resolves client IP only once per
connection.
This commit is contained in:
Sergey Kuznetsov
2026-04-22 16:01:18 +01:00
committed by GitHub
parent e091175be7
commit d7bcf6e726
7 changed files with 288 additions and 34 deletions

View File

@@ -8,6 +8,7 @@
#include <boost/beast/http/field.hpp>
#include <algorithm>
#include <optional>
#include <string>
#include <string_view>
@@ -49,20 +50,20 @@ ProxyIpResolver::fromConfig(util::config::ClioConfigDefinition const& config)
return ProxyIpResolver{std::move(ips), std::move(tokens)};
}
std::string
std::optional<std::string>
ProxyIpResolver::resolveClientIp(std::string const& connectionIp, HttpHeaders const& headers) const
{
if (proxyIps_.contains(connectionIp)) {
return extractClientIp(headers).value_or(connectionIp);
return extractClientIp(headers);
}
if (auto it = headers.find(kPROXY_TOKEN_HEADER); it != headers.end()) {
auto const tokenHash = util::sha256sum(it->value());
if (proxyTokens_.contains(tokenHash)) {
return extractClientIp(headers).value_or(connectionIp);
return extractClientIp(headers);
}
}
return connectionIp;
return std::nullopt;
}
std::optional<std::string>
@@ -78,14 +79,17 @@ ProxyIpResolver::extractClientIp(HttpHeaders const& headers)
auto const headerValue = util::toLower(it->value());
static constexpr std::string_view kFOR_PREFIX = "for=";
auto const startPos = headerValue.find(kFOR_PREFIX);
auto const startPos = headerValue.rfind(kFOR_PREFIX);
if (startPos == std::string::npos) {
return std::nullopt;
}
auto value = it->value().substr(startPos + kFOR_PREFIX.size());
static constexpr char kDELIMITER = ';';
auto const endPos = value.find(kDELIMITER);
static constexpr char kSECTION_DELIMITER = ';';
static constexpr char kCHAIN_DELIMITER = ',';
auto const sectionEnd = value.find(kSECTION_DELIMITER);
auto const chainEnd = value.find(kCHAIN_DELIMITER);
auto const endPos = std::min(sectionEnd, chainEnd);
auto const ip = value.substr(0, endPos);
static constexpr auto kMIN_IP_LENGTH = 7; // minimum 3 dots + 4 digits

View File

@@ -59,16 +59,16 @@ public:
*
* If the connection IP is in the trusted proxy list, or if a valid proxy token is provided in
* the headers, this method will attempt to extract the client's IP from the `Forwarded` header.
* Otherwise, it returns the connection IP.
* Otherwise, returns std::nullopt.
*
* @param connectionIp The IP address of the direct connection.
* @param headers The HTTP request headers.
* @return The resolved client IP address as a string.
* @return The resolved client IP address if the connection is from a trusted proxy, otherwise
* std::nullopt.
*/
std::string
std::optional<std::string>
resolveClientIp(std::string const& connectionIp, HttpHeaders const& headers) const;
private:
/**
* @brief Extracts the client IP from the `Forwarded` HTTP header.
*

View File

@@ -122,6 +122,7 @@ class HttpBase : public ConnectionBase {
SendLambda sender_;
std::shared_ptr<AdminVerificationStrategy> adminVerification_;
std::shared_ptr<ProxyIpResolver> proxyIpResolver_;
bool isProxyConnection_ = false;
protected:
boost::beast::flat_buffer buffer_;
@@ -222,14 +223,26 @@ public:
if (ec)
return httpFail(ec, "read");
if (auto resolvedIp = proxyIpResolver_->resolveClientIp(clientIp_, req_);
resolvedIp != clientIp_) {
auto const updateClientIp = [&](std::string newIp) {
if (newIp == clientIp_)
return;
LOG(log_.info()) << tag()
<< "Detected a forwarded request from proxy. Proxy ip: " << clientIp_
<< ". Resolved client ip: " << resolvedIp;
<< "Detected a forwarded request from proxy. Resolved client ip: "
<< newIp;
dosGuard_.get().decrement(clientIp_);
clientIp_ = std::move(resolvedIp);
clientIp_ = std::move(newIp);
dosGuard_.get().increment(clientIp_);
};
if (isProxyConnection_) {
if (auto resolvedIp = ProxyIpResolver::extractClientIp(req_); resolvedIp.has_value())
updateClientIp(std::move(*resolvedIp));
} else if (
auto resolvedIp = proxyIpResolver_->resolveClientIp(clientIp_, req_);
resolvedIp.has_value()
) {
updateClientIp(std::move(*resolvedIp));
isProxyConnection_ = true;
}
if (req_.method() == http::verb::get and req_.target() == "/health")

View File

@@ -26,6 +26,7 @@ class ConnectionMetadata : public util::Taggable {
protected:
std::string ip_; // client ip
std::optional<bool> isAdmin_;
bool isProxyConnection_ = false;
public:
/**
@@ -63,6 +64,26 @@ public:
ip_ = std::move(newIp);
}
/**
* @brief Mark this connection as coming through a trusted proxy.
*/
void
markAsProxyConnection()
{
isProxyConnection_ = true;
}
/**
* @brief Whether this connection was identified as coming through a trusted proxy.
*
* @return true if the connection is a proxy connection.
*/
[[nodiscard]] bool
isProxyConnection() const
{
return isProxyConnection_;
}
/**
* @brief Get whether the client is an admin.
*

View File

@@ -402,14 +402,26 @@ ConnectionHandler::handleRequest(
void
ConnectionHandler::resolveClientIp(Connection& connection, Request const& request) const
{
if (auto resolvedClientIp =
proxyIpResolver_.resolveClientIp(connection.ip(), request.httpHeaders());
resolvedClientIp != connection.ip()) {
auto const updateIp = [&](std::string newIp) {
if (newIp == connection.ip())
return;
LOG(log_.info()) << connection.tag()
<< "Detected a forwarded request from proxy. Proxy ip: " << connection.ip()
<< ". Resolved client ip: " << resolvedClientIp;
onIpChangeHook_(connection.ip(), resolvedClientIp);
connection.setIp(std::move(resolvedClientIp));
<< "Detected a forwarded request from proxy. Resolved client ip: "
<< newIp;
onIpChangeHook_(connection.ip(), newIp);
connection.setIp(std::move(newIp));
};
if (connection.isProxyConnection()) {
if (auto resolvedIp = ProxyIpResolver::extractClientIp(request.httpHeaders());
resolvedIp.has_value())
updateIp(std::move(*resolvedIp));
} else if (
auto resolvedIp = proxyIpResolver_.resolveClientIp(connection.ip(), request.httpHeaders());
resolvedIp.has_value()
) {
updateIp(std::move(*resolvedIp));
connection.markAsProxyConnection();
}
}