Closed DvoryankinEvgeny closed 1 month ago
To simplify reproducing the bug I implemented integration tests:
static const std::string my_config = R"EOF(
name: typed-per-filter-config-for-ws
virtual_hosts:
- name: app-ws
domains:
- "*"
routes:
- match:
prefix: "/disable-basic-auth"
direct_response:
status: 200
body:
inline_string: "OK"
typed_per_filter_config:
envoy.filters.http.basic_auth:
"@type": type.googleapis.com/envoy.config.route.v3.FilterConfig
disabled: true
- match:
prefix: "/ws-disable-basic-auth"
route:
cluster: cluster_0
upgrade_configs:
- upgrade_type: websocket
typed_per_filter_config:
envoy.filters.http.basic_auth:
"@type": type.googleapis.com/envoy.config.route.v3.FilterConfig
disabled: true
- match:
prefix: "/ws"
route:
cluster: cluster_0
upgrade_configs:
- upgrade_type: websocket
- match:
prefix: "/"
direct_response:
status: 200
body:
inline_string: "OK"
)EOF";
static const std::string my_filter = R"EOF(
name: envoy.filters.http.basic_auth
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.basic_auth.v3.BasicAuth
users:
inline_string: |-
user:{SHA}W6ph5Mm5Pz8GgiULbPgzG37mj9g=
)EOF";
class MyIntegrationTest : public Envoy::HttpIntegrationTest, public testing::Test {
public:
MyIntegrationTest()
: HttpIntegrationTest(Http::CodecType::HTTP1, Network::Address::IpVersion::v4) {}
void initializeRoutes() {
config_helper_.addConfigModifier(
[&](envoy::extensions::filters::network::http_connection_manager::v3::HttpConnectionManager&
hcm) -> void {
auto route_config = hcm.mutable_route_config();
TestUtility::loadFromYaml(my_config, *route_config);
});
initialize();
}
void addBasicAuthFilter() { config_helper_.prependFilter(my_filter); }
void performTest() {
codec_client_ = makeHttpConnection(lookupPort("http"));
Http::TestRequestHeaderMapImpl request_headers{
{":method", "GET"},
{":path", "/ws-disable-basic-auth"},
{":scheme", "http"},
{":authority", "host"},
{"connection", "Upgrade"},
{"upgrade", "websocket"},
{"sec-websocket-key", "dGhlIHNhbXBsZSBub25jZQ=="},
{"sec-websocket-version", "13"}};
Http::TestRequestHeaderMapImpl response_headers{
{":status", "101"},
{"connection", "Upgrade"},
{"upgrade", "websocket"},
{"Sec-WebSocket-Accept", "EDJa7WCAQQzMCYNJM42Syuo9SqQ="}};
auto encoder_decoder = codec_client_->startRequest(request_headers);
request_encoder_ = &encoder_decoder.first;
auto response = std::move(encoder_decoder.second);
ASSERT_TRUE(fake_upstreams_[0]->waitForHttpConnection(*dispatcher_, fake_upstream_connection_));
ASSERT_TRUE(fake_upstream_connection_->waitForNewStream(*dispatcher_, upstream_request_));
ASSERT_TRUE(upstream_request_->waitForHeadersComplete());
// Accept websocket upgrade request
upstream_request_->encodeHeaders(response_headers, false);
response->waitForHeaders();
EXPECT_EQ("101", response->headers().getStatusValue());
cleanupUpstreamAndDownstream();
}
};
TEST_F(MyIntegrationTest, SuccessfulWebsocketUpgradeWithoutBasicAuthFilter) {
initializeRoutes();
performTest();
}
TEST_F(MyIntegrationTest, WebsocketUpgradeFailsWhenBasicAuthFilterPresnted) {
addBasicAuthFilter();
initializeRoutes();
performTest();
}
Thank you for the reproduction @DvoryankinEvgeny . ISTM like a bug, possibly here: https://github.com/envoyproxy/envoy/blob/f79b881883e862bc0f7dc7f09d3bc811fb0944f6/source/common/http/filter_manager.cc#L1571
Where we create the upgraded filter chain and not setting the route info.
I wonder if you flip the filter to be default off but enabled for the routes if that would be a hacky workaround.
Thank you for the reproduction @DvoryankinEvgeny . ISTM like a bug, possibly here:
Where we create the upgraded filter chain and not setting the route info.
I wonder if you flip the filter to be default off but enabled for the routes if that would be a hacky workaround.
Hello Kevin,
Thank you for the response.
I am quite novice with the envoy, so I am struggling with making basic_auth disabled by default. If I add disabled: true
like this:
http_filters:
- name: envoy.filters.http.basic_auth
disabled: true
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.basic_auth.v3.BasicAuth
users:
inline_string: |-
user:{SHA}W6ph5Mm5Pz8GgiULbPgzG37mj9g=
then envoy start fails with error _Empty route/virtual host per filter configuration for envoy.filters.http.basicauth filter Could you please help with configuration?
@KBaichoo hi,
I think I found the problem. It seems to me, that the root case is here:
https://github.com/envoyproxy/envoy/blob/189dd4700f823f2cbbef482c61d50a34e16ff4f2/source/extensions/filters/network/http_connection_manager/config.cc#L810-L811
We pass an instance of Http::EmptyFilterChainOptions
to the call of Http::FilterChainUtility::createFilterChainForFactories()
.
Implementation of Http::EmptyFilterChainOptions::filterDisabled()
always returns a default-constructed absl::optional<bool>
, so when we come to the FilterChainUtility::createFilterChainForFactories()
call,
https://github.com/envoyproxy/envoy/blob/189dd4700f823f2cbbef482c61d50a34e16ff4f2/source/common/http/filter_chain_helper.cc#L17-L26
we call Http::EmptyFilterChainOptions::filterDisabled()
to check if a filter is disabled, and since it always returns a default-constructed absl::optional<bool>
we think, that all filters are enabled.
As one of the possible solutions I tried to extend the signature of the createUpgradeFilterChain()
method declared here
https://github.com/envoyproxy/envoy/blob/189dd4700f823f2cbbef482c61d50a34e16ff4f2/envoy/http/filter_factory.h#L116-L119
with a new parameter const Http::FilterChainOptions& option = Http::EmptyFilterChainOptions{}
.
In the implementation of HttpConnectionManagerConfig::createUpgradeFilterChain
I passed this new parameter option
into the call Http::FilterChainUtility::createFilterChainForFactories
instead of Http::EmptyFilterChainOptions
here https://github.com/envoyproxy/envoy/blob/189dd4700f823f2cbbef482c61d50a34e16ff4f2/source/extensions/filters/network/http_connection_manager/config.cc#L810-L811
In FilterManager::createFilterChain()
moved up a little bit creation of FilterChainOptionsImpl option
https://github.com/envoyproxy/envoy/blob/189dd4700f823f2cbbef482c61d50a34e16ff4f2/source/common/http/filter_manager.cc#L1582-L1583 and passed this option
into createUpgradeFilterChain
call here https://github.com/envoyproxy/envoy/blob/189dd4700f823f2cbbef482c61d50a34e16ff4f2/source/common/http/filter_manager.cc#L1569-L1570
After all these manipulation my tests started to work as expected :)
So, I have two questions:
I created a draft PR to show what I actually want to change. Hope it will simplify understanding. https://github.com/envoyproxy/envoy/pull/35292
Title: Upgrade to Websocket doesn't ignore disabled 'typed_per_filter_config' for this route
Description: We have a configuration where we have two http filters: basic_auth and router. We also have a set of routes:
/
: all request to the root must be authorized through basic authdisable-basic-auth
: for this route we use typed_per_filter_config to disable basic auth filterThis configuration works as expected:
Request to the root without credentials rejected
Request to the root with correct pair of username and password succeeded.(OK is direct_response from envoy)
Request without credentials to the endpoint with disabled basic authorization also works fine.
By analogy with those endpoints we have two endpoints for WebSocket connections with upgrade_configs and upgrade_type equal to websocket:
/ws
: endpoint with configured upgrade_configs/ws-disable-basic-auth
: same upgrade_config but with typed_per_filter_config to disable basic auth filterRequest to the first Websocket endpoint finishes with 401 Unauthorized as expected because we didn't disabled basic auth for this endpoint.
Request to /ws-disable-basic-auth is expected to be successfully passed to the upstream, but unfortunately it is not.
Repro steps:
Create docker-compose.yaml file with following content:
networks: my_network: ipam: config:
admin: address: socket_address: address: 0.0.0.0 port_value: 9902 static_resources: listeners:
Logs: I attached two logs files: