Skip to content

Commit

Permalink
lua: add new function to set upstream host override (#37327)
Browse files Browse the repository at this point in the history
## Description

Adds new functionality to Lua filter flor allowing overriding the
upstream host address.

**Example:**
```lua
function envoy_on_request(request_handle)
  request_handle:setUpstreamOverrideHost("192.168.21.11", true)
end
```

---

**Commit Message:** adds new function to the LUA script for setting
upstream host override
**Additional Description:** The new `setUpstreamOverrideHost()` allows
the dynamic upstream host override from Lua scripts.
**Risk Level:** Low
**Testing:** Added Unit + Integration tests
**Docs Changes:** Added
**Release Notes:** Added

---------

Signed-off-by: Rohit Agrawal <[email protected]>
  • Loading branch information
agrawroh authored Nov 25, 2024
1 parent e1b2d7b commit 256f107
Show file tree
Hide file tree
Showing 5 changed files with 219 additions and 1 deletion.
4 changes: 4 additions & 0 deletions changelogs/current.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,10 @@ new_features:
change: |
Added support in SNI dynamic forward proxy for saving the resolved upstream address in the filter state.
The state is saved with the key ``envoy.stream.upstream_address``.
- area: lua
change: |
Added a new ``setUpstreamOverrideHost()`` which could be used to set the given host as the upstream host for the
current request.
deprecated:
- area: rbac
Expand Down
30 changes: 30 additions & 0 deletions docs/root/configuration/http/http_filters/lua_filter.rst
Original file line number Diff line number Diff line change
Expand Up @@ -558,6 +558,36 @@ Returns connection-level :repo:`information <envoy/stream_info/stream_info.h>` r

Returns a connection-level :ref:`stream info object <config_http_filters_lua_cx_stream_info_wrapper>`.

``setUpstreamOverrideHost()``
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

.. code-block:: lua
handle:setUpstreamOverrideHost(host, strict)
Sets an upstream address override for the request. When the overridden host is available and can be selected directly,
the load balancer bypasses its algorithm and routes traffic directly to the specified host. The strict flag determines
whether the HTTP request must strictly use the overridden destination. If the destination is unavailable and strict is
set to true, Envoy responds with a 503 Service Unavailable error.

The function takes two arguments:

* ``host`` (string): The host address to be used for upstream requests.
* ``strict`` (boolean, optional): If set to true, the request is strictly routed to the overridden host. If the host is
unavailable, Envoy returns a 503 error. Defaults to false.

Example:

.. code-block:: lua
function envoy_on_request(request_handle)
-- Override upstream host without strict mode
request_handle:setUpstreamOverrideHost("192.168.21.13", false)
-- Override upstream host with strict mode
request_handle:setUpstreamOverrideHost("192.168.21.13", true)
end
``importPublicKey()``
^^^^^^^^^^^^^^^^^^^^^

Expand Down
26 changes: 26 additions & 0 deletions source/extensions/filters/http/lua/lua_filter.cc
Original file line number Diff line number Diff line change
Expand Up @@ -963,6 +963,32 @@ void Filter::scriptLog(spdlog::level::level_enum level, absl::string_view messag
}
}

int StreamHandleWrapper::luaSetUpstreamOverrideHost(lua_State* state) {
// Get the host address argument
size_t len;
const char* host = luaL_checklstring(state, 2, &len);

// Validate that host is not null and is an IP address
if (host == nullptr) {
luaL_error(state, "host argument is required");
}
if (!Http::Utility::parseAuthority(host).is_ip_address_) {
luaL_error(state, "host is not a valid IP address");
}

// Get the optional strict flag (defaults to false)
bool strict = false;
if (lua_gettop(state) >= 3) {
luaL_checktype(state, 3, LUA_TBOOLEAN);
strict = lua_toboolean(state, 3);
}

// Set the upstream override host
callbacks_.setUpstreamOverrideHost(std::make_pair(std::string(host, len), strict));

return 0;
}

void Filter::DecoderCallbacks::respond(Http::ResponseHeaderMapPtr&& headers, Buffer::Instance* body,
lua_State*) {
uint64_t status = Http::Utility::getResponseStatus(*headers);
Expand Down
22 changes: 21 additions & 1 deletion source/extensions/filters/http/lua/lua_filter.h
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,12 @@ class FilterCallbacks {
* @return const Tracing::Span& the current tracing active span.
*/
virtual Tracing::Span& activeSpan() PURE;

/**
* Set the upstream host override.
* @param host_and_strict supplies the host and whether the host should be treated as strict.
*/
virtual void setUpstreamOverrideHost(std::pair<std::string, bool> host_and_strict) PURE;
};

class Filter;
Expand Down Expand Up @@ -187,7 +193,8 @@ class StreamHandleWrapper : public Filters::Common::Lua::BaseLuaObject<StreamHan
{"base64Escape", static_luaBase64Escape},
{"timestamp", static_luaTimestamp},
{"timestampString", static_luaTimestampString},
{"connectionStreamInfo", static_luaConnectionStreamInfo}};
{"connectionStreamInfo", static_luaConnectionStreamInfo},
{"setUpstreamOverrideHost", static_luaSetUpstreamOverrideHost}};
}

private:
Expand Down Expand Up @@ -318,6 +325,13 @@ class StreamHandleWrapper : public Filters::Common::Lua::BaseLuaObject<StreamHan
*/
DECLARE_LUA_FUNCTION(StreamHandleWrapper, luaTimestampString);

/**
* Set the upstream override host.
* @param 1 (string): The host address to override with.
* @param 2 (bool): Optional strict flag. Defaults to false.
*/
DECLARE_LUA_FUNCTION(StreamHandleWrapper, luaSetUpstreamOverrideHost);

enum Timestamp::Resolution getTimestampResolution(absl::string_view unit_parameter);

int doHttpCall(lua_State* state, const HttpCallOptions& options);
Expand Down Expand Up @@ -571,6 +585,9 @@ class Filter : public Http::StreamFilter, Logger::Loggable<Logger::Id::lua> {
return callbacks_->connection().ptr();
}
Tracing::Span& activeSpan() override { return callbacks_->activeSpan(); }
void setUpstreamOverrideHost(std::pair<std::string, bool> host_and_strict) override {
callbacks_->setUpstreamOverrideHost(std::move(host_and_strict));
}

Filter& parent_;
Http::StreamDecoderFilterCallbacks* callbacks_{};
Expand All @@ -595,6 +612,9 @@ class Filter : public Http::StreamFilter, Logger::Loggable<Logger::Id::lua> {
return callbacks_->connection().ptr();
}
Tracing::Span& activeSpan() override { return callbacks_->activeSpan(); }
void setUpstreamOverrideHost(std::pair<std::string, bool> host_and_strict) override {
UNREFERENCED_PARAMETER(host_and_strict);
}

Filter& parent_;
Http::StreamEncoderFilterCallbacks* callbacks_{};
Expand Down
138 changes: 138 additions & 0 deletions test/extensions/filters/http/lua/lua_filter_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -3033,6 +3033,144 @@ TEST_F(LuaHttpFilterTest, StatsWithPerFilterPrefix) {
EXPECT_EQ(2, stats_store_.counter("test.lua.my_script.errors").value());
}

// Test successful upstream host override
TEST_F(LuaHttpFilterTest, SetUpstreamOverrideHost) {
const std::string SCRIPT{R"EOF(
function envoy_on_request(request_handle)
request_handle:setUpstreamOverrideHost("192.168.21.11", false)
end
)EOF"};

InSequence s;
setup(SCRIPT);

Http::TestRequestHeaderMapImpl request_headers{{":path", "/"}};
EXPECT_CALL(decoder_callbacks_,
setUpstreamOverrideHost(testing::Pair(testing::Eq("192.168.21.11"), false)));
EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->decodeHeaders(request_headers, true));
}

// Test upstream host override with strict flag set to true
TEST_F(LuaHttpFilterTest, SetUpstreamOverrideHostStrict) {
const std::string SCRIPT{R"EOF(
function envoy_on_request(request_handle)
request_handle:setUpstreamOverrideHost("192.168.21.11", true)
end
)EOF"};

InSequence s;
setup(SCRIPT);

Http::TestRequestHeaderMapImpl request_headers{{":path", "/"}};
EXPECT_CALL(decoder_callbacks_,
setUpstreamOverrideHost(testing::Pair(testing::Eq("192.168.21.11"), true)));
EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->decodeHeaders(request_headers, true));
}

// Test that setUpstreamOverrideHost requires a host argument
TEST_F(LuaHttpFilterTest, SetUpstreamOverrideHostNoArgument) {
const std::string SCRIPT{R"EOF(
function envoy_on_request(request_handle)
request_handle:setUpstreamOverrideHost()
end
)EOF"};

InSequence s;
setup(SCRIPT);

Http::TestRequestHeaderMapImpl request_headers{{":path", "/"}};
EXPECT_CALL(*filter_,
scriptLog(spdlog::level::err,
StrEq("[string \"...\"]:3: bad argument #1 to 'setUpstreamOverrideHost' "
"(string expected, got no value)")));
EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->decodeHeaders(request_headers, true));
EXPECT_EQ(1, stats_store_.counter("test.lua.errors").value());
}

// Test that setUpstreamOverrideHost validates the argument type for strict flag
TEST_F(LuaHttpFilterTest, SetUpstreamOverrideHostInvalidStrictType) {
const std::string SCRIPT{R"EOF(
function envoy_on_request(request_handle)
request_handle:setUpstreamOverrideHost("192.168.21.11", "not_a_boolean")
end
)EOF"};

InSequence s;
setup(SCRIPT);

Http::TestRequestHeaderMapImpl request_headers{{":path", "/"}};
EXPECT_CALL(*filter_,
scriptLog(spdlog::level::err,
StrEq("[string \"...\"]:3: bad argument #2 to 'setUpstreamOverrideHost' "
"(boolean expected, got string)")));
EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->decodeHeaders(request_headers, true));
EXPECT_EQ(1, stats_store_.counter("test.lua.errors").value());
}

// Test that setUpstreamOverrideHost can be called on different paths
TEST_F(LuaHttpFilterTest, SetUpstreamOverrideHostDifferentPaths) {
const std::string SCRIPT{R"EOF(
function envoy_on_request(request_handle)
request_handle:setUpstreamOverrideHost("192.168.21.11", true)
end
)EOF"};

InSequence s;
setup(SCRIPT);

{
Http::TestRequestHeaderMapImpl request_headers{{":path", "/path1"}};
EXPECT_CALL(decoder_callbacks_,
setUpstreamOverrideHost(testing::Pair(testing::Eq("192.168.21.11"), true)));
EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->decodeHeaders(request_headers, true));
}

setupFilter();

{
Http::TestRequestHeaderMapImpl request_headers{{":path", "/path2"}};
EXPECT_CALL(decoder_callbacks_,
setUpstreamOverrideHost(testing::Pair(testing::Eq("192.168.21.11"), true)));
EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->decodeHeaders(request_headers, true));
}
}

// Test empty host argument
TEST_F(LuaHttpFilterTest, SetUpstreamOverrideHostEmptyHost) {
const std::string SCRIPT{R"EOF(
function envoy_on_request(request_handle)
request_handle:setUpstreamOverrideHost("", false)
end
)EOF"};

InSequence s;
setup(SCRIPT);

Http::TestRequestHeaderMapImpl request_headers{{":path", "/"}};
EXPECT_CALL(*filter_, scriptLog(spdlog::level::err,
StrEq("[string \"...\"]:3: host is not a valid IP address")));
EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->decodeHeaders(request_headers, true));
EXPECT_EQ(1, stats_store_.counter("test.lua.errors").value());
}

// Test that setUpstreamOverrideHost rejects non-IP hosts
TEST_F(LuaHttpFilterTest, SetUpstreamOverrideHostNonIpHost) {
const std::string SCRIPT{R"EOF(
function envoy_on_request(request_handle)
request_handle:setUpstreamOverrideHost("example.com", false)
end
)EOF"};

InSequence s;
setup(SCRIPT);

Http::TestRequestHeaderMapImpl request_headers{{":path", "/"}};
EXPECT_CALL(*filter_, scriptLog(spdlog::level::err,
StrEq("[string \"...\"]:3: host is not a valid IP address")));
EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->decodeHeaders(request_headers, true));
EXPECT_EQ(1, stats_store_.counter("test.lua.errors").value());
}

} // namespace
} // namespace Lua
} // namespace HttpFilters
Expand Down

0 comments on commit 256f107

Please sign in to comment.