fix: Proxy support (#3103)

Port of two changes onto release/2.7.1:
- #3006 (d3381a1d): resolve proxy ip before processing any request
- #3043 (d7bcf6e7): re-resolve client ip when a proxy reuses a TCP
connection for different clients (resolveClientIp now returns
std::optional; extractClientIp made public; isProxyConnection_ tracked)
This commit is contained in:
Sergey Kuznetsov
2026-06-09 16:25:02 +01:00
committed by GitHub
parent ce44aec245
commit 14645e1494
7 changed files with 252 additions and 36 deletions

View File

@@ -523,6 +523,88 @@ TEST_F(ConnectionHandlerSequentialProcessingTest, OnIpChangeHookCalledWhenSentFr
});
}
TEST_F(ConnectionHandlerSequentialProcessingTest, ProxyConnection_SameClientReuses_HookCalledOnce)
{
std::string const target = "/some/target";
testing::StrictMock<testing::MockFunction<
Response(Request const&, ConnectionMetadata const&, web::SubscriptionContextPtr, boost::asio::yield_context)>>
getHandlerMock;
connectionHandler.onGet(target, getHandlerMock.AsStdFunction());
StrictMockHttpConnectionPtr mockProxyConnection =
std::make_unique<StrictMockHttpConnection>(proxyIp, boost::beast::flat_buffer{}, tagDecoratorFactory);
auto request = http::request<http::string_body>{http::verb::get, target, 11, ""};
request.set(http::field::forwarded, fmt::format("for={}", clientIp));
EXPECT_CALL(*mockProxyConnection, wasUpgraded).WillOnce(Return(false));
EXPECT_CALL(*mockProxyConnection, receive)
.WillOnce(Return(makeRequest(request)))
.WillOnce(Return(makeRequest(request)))
.WillOnce(Return(makeError(http::error::end_of_stream)));
EXPECT_CALL(onIpChangeMock, Call(proxyIp, clientIp));
EXPECT_CALL(getHandlerMock, Call).Times(2).WillRepeatedly([](Request const& req, auto&&, auto&&, auto&&) {
return Response(http::status::ok, "ok", req);
});
EXPECT_CALL(*mockProxyConnection, send).Times(2).WillRepeatedly(Return(std::expected<void, web::ng::Error>{}));
EXPECT_CALL(onDisconnectMock, Call).WillOnce([this, ptr = mockProxyConnection.get()](Connection const& c) {
EXPECT_EQ(&c, ptr);
EXPECT_EQ(c.ip(), clientIp);
});
runSpawn([this, c = std::move(mockProxyConnection)](boost::asio::yield_context yield) mutable {
connectionHandler.processConnection(std::move(c), yield);
});
}
TEST_F(ConnectionHandlerSequentialProcessingTest, ProxyConnection_DifferentClientReuses_HookCalledForEachIpChange)
{
std::string const target = "/some/target";
std::string const anotherClientIp = "9.10.11.12";
testing::StrictMock<testing::MockFunction<
Response(Request const&, ConnectionMetadata const&, web::SubscriptionContextPtr, boost::asio::yield_context)>>
getHandlerMock;
connectionHandler.onGet(target, getHandlerMock.AsStdFunction());
StrictMockHttpConnectionPtr mockProxyConnection =
std::make_unique<StrictMockHttpConnection>(proxyIp, boost::beast::flat_buffer{}, tagDecoratorFactory);
auto request1 = http::request<http::string_body>{http::verb::get, target, 11, ""};
request1.set(http::field::forwarded, fmt::format("for={}", clientIp));
auto request2 = http::request<http::string_body>{http::verb::get, target, 11, ""};
request2.set(http::field::forwarded, fmt::format("for={}", anotherClientIp));
EXPECT_CALL(*mockProxyConnection, wasUpgraded).WillOnce(Return(false));
EXPECT_CALL(*mockProxyConnection, receive)
.WillOnce(Return(makeRequest(request1)))
.WillOnce(Return(makeRequest(request2)))
.WillOnce(Return(makeError(http::error::end_of_stream)));
EXPECT_CALL(onIpChangeMock, Call(proxyIp, clientIp));
EXPECT_CALL(onIpChangeMock, Call(clientIp, anotherClientIp));
EXPECT_CALL(getHandlerMock, Call).Times(2).WillRepeatedly([](Request const& req, auto&&, auto&&, auto&&) {
return Response(http::status::ok, "ok", req);
});
EXPECT_CALL(*mockProxyConnection, send).Times(2).WillRepeatedly(Return(std::expected<void, web::ng::Error>{}));
EXPECT_CALL(onDisconnectMock, Call)
.WillOnce([anotherClientIp, ptr = mockProxyConnection.get()](Connection const& c) {
EXPECT_EQ(&c, ptr);
EXPECT_EQ(c.ip(), anotherClientIp);
});
runSpawn([this, c = std::move(mockProxyConnection)](boost::asio::yield_context yield) mutable {
connectionHandler.processConnection(std::move(c), yield);
});
}
TEST_F(ConnectionHandlerSequentialProcessingTest, Stop)
{
testing::StrictMock<testing::MockFunction<
@@ -728,6 +810,75 @@ TEST_F(ConnectionHandlerParallelProcessingTest, OnIpChangeHookCalledWhenSentFrom
});
}
TEST_F(ConnectionHandlerParallelProcessingTest, ProxyConnection_SameClientReuses_HookCalledOnce)
{
connectionHandler.onWs([](Request const& req, auto&&, auto&&, auto&&) {
return Response(http::status::ok, "ok", req);
});
StrictMockWsConnectionPtr mockProxyConnection =
std::make_unique<StrictMockWsConnection>(proxyIp, boost::beast::flat_buffer{}, tagDecoratorFactory);
headers.set(http::field::forwarded, fmt::format("for={}", clientIp));
EXPECT_CALL(*mockProxyConnection, wasUpgraded).WillOnce(Return(true));
EXPECT_CALL(*mockProxyConnection, receive)
.WillOnce(Return(makeRequest("msg", headers)))
.WillOnce(Return(makeRequest("msg", headers)))
.WillOnce(Return(makeError(websocket::error::closed)));
EXPECT_CALL(onIpChangeMock, Call(proxyIp, clientIp));
EXPECT_CALL(*mockProxyConnection, send).Times(2).WillRepeatedly(Return(std::expected<void, web::ng::Error>{}));
EXPECT_CALL(onDisconnectMock, Call).WillOnce([this, ptr = mockProxyConnection.get()](Connection const& c) {
EXPECT_EQ(&c, ptr);
EXPECT_EQ(c.ip(), clientIp);
});
runSpawn([this, c = std::move(mockProxyConnection)](boost::asio::yield_context yield) mutable {
connectionHandler.processConnection(std::move(c), yield);
});
}
TEST_F(ConnectionHandlerParallelProcessingTest, ProxyConnection_DifferentClientReuses_HookCalledForEachIpChange)
{
std::string const anotherClientIp = "9.10.11.12";
connectionHandler.onWs([](Request const& req, auto&&, auto&&, auto&&) {
return Response(http::status::ok, "ok", req);
});
StrictMockWsConnectionPtr mockProxyConnection =
std::make_unique<StrictMockWsConnection>(proxyIp, boost::beast::flat_buffer{}, tagDecoratorFactory);
Request::HttpHeaders headers1;
headers1.set(http::field::forwarded, fmt::format("for={}", clientIp));
Request::HttpHeaders headers2;
headers2.set(http::field::forwarded, fmt::format("for={}", anotherClientIp));
EXPECT_CALL(*mockProxyConnection, wasUpgraded).WillOnce(Return(true));
EXPECT_CALL(*mockProxyConnection, receive)
.WillOnce(Return(makeRequest("msg", headers1)))
.WillOnce(Return(makeRequest("msg", headers2)))
.WillOnce(Return(makeError(websocket::error::closed)));
EXPECT_CALL(onIpChangeMock, Call(proxyIp, clientIp));
EXPECT_CALL(onIpChangeMock, Call(clientIp, anotherClientIp));
EXPECT_CALL(*mockProxyConnection, send).Times(2).WillRepeatedly(Return(std::expected<void, web::ng::Error>{}));
EXPECT_CALL(onDisconnectMock, Call)
.WillOnce([anotherClientIp, ptr = mockProxyConnection.get()](Connection const& c) {
EXPECT_EQ(&c, ptr);
EXPECT_EQ(c.ip(), anotherClientIp);
});
runSpawn([this, c = std::move(mockProxyConnection)](boost::asio::yield_context yield) mutable {
connectionHandler.processConnection(std::move(c), yield);
});
}
TEST_F(ConnectionHandlerParallelProcessingTest, Receive_Handle_Send_Loop)
{
testing::StrictMock<testing::MockFunction<