Skip to content

Commit

Permalink
listener: add match all filter chain (envoyproxy#13449) (#276)
Browse files Browse the repository at this point in the history
The match all filter chain is chosen when no other filter chain matches
the request.

Signed-off-by: Yuchen Dai <[email protected]>
  • Loading branch information
lambdai authored Oct 16, 2020
1 parent 37f7aac commit 14a39e3
Show file tree
Hide file tree
Showing 19 changed files with 353 additions and 57 deletions.
6 changes: 5 additions & 1 deletion api/envoy/config/listener/v3/listener.proto
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ message ListenerCollection {
repeated udpa.core.v1.CollectionEntry entries = 1;
}

// [#next-free-field: 25]
// [#next-free-field: 26]
message Listener {
option (udpa.annotations.versioning).previous_message_type = "envoy.api.v2.Listener";

Expand Down Expand Up @@ -116,6 +116,10 @@ message Listener {
// :ref:`FAQ entry <faq_how_to_setup_sni>`.
repeated FilterChain filter_chains = 3;

// The default filter chain if none of the filter chain matches. If no default filter chain is supplied,
// the connection will be closed. The filter chain match is ignored in this field.
FilterChain default_filter_chain = 25;

// Soft limit on size of the listener’s new connection read and write buffers.
// If unspecified, an implementation defined default is applied (1MiB).
google.protobuf.UInt32Value per_connection_buffer_limit_bytes = 5
Expand Down
12 changes: 12 additions & 0 deletions api/envoy/config/listener/v3/listener_components.proto
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,18 @@ message Filter {
// ``www.example.com``, then ``*.example.com``, then ``*.com``, then any filter
// chain without ``server_names`` requirements).
//
// A different way to reason about the filter chain matches:
// Suppose there exists N filter chains. Prune the filter chain set using the above 8 steps.
// In each step, filter chains which most specifically matches the attributes continue to the next step.
// The listener guarantees at most 1 filter chain is left after all of the steps.
//
// Example:
//
// For destination port, filter chains specifying the destination port of incoming traffic are the
// most specific match. If none of the filter chains specifies the exact destination port, the filter
// chains which do not specify ports are the most specific match. Filter chains specifying the
// wrong port can never be the most specific match.
//
// [#comment: Implemented rules are kept in the preference order, with deprecated fields
// listed at the end, because that's how we want to list them in the docs.
//
Expand Down
6 changes: 5 additions & 1 deletion api/envoy/config/listener/v4alpha/listener.proto

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions api/envoy/config/listener/v4alpha/listener_components.proto

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 6 additions & 1 deletion docs/root/configuration/listeners/lds.rst
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,15 @@ The semantics of listener updates are as follows:
* Listeners are effectively constant once created. Thus, when a listener is updated, an entirely
new listener is created (with the same listen socket). This listener goes through the same
warming process described above for a newly added listener.
* When a listener is updated or removed, the old listener will be placed into a "draining" state
* When a listener is removed, the old listener will be placed into a "draining" state
much like when the entire server is drained for restart. Connections owned by the listener will
be gracefully closed (if possible) for some period of time before the listener is removed and any
remaining connections are closed. The drain time is set via the :option:`--drain-time-s` option.
* When a tcp listener is updated, if the new listener contains a subset of filter chains in the old listener,
the connections owned by these overlapping filter chains remain open. Only the connections owned by the
removed filter chains will be drained following the above pattern. Note that if any global listener attributes are
changed, the entire listener (and all filter chains) are drained similar to removal above. See
:ref:`filter chain only update <filter_chain_only_update>` for detailed rules to reason about the impacted filter chains.

.. note::

Expand Down
1 change: 1 addition & 0 deletions docs/root/intro/arch_overview/listeners/listeners_toc.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ Listeners

listeners
listener_filters
network_filter_chain
network_filters
tcp_proxy
udp_proxy
Expand Down
30 changes: 30 additions & 0 deletions docs/root/intro/arch_overview/listeners/network_filter_chain.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
.. _arch_overview_network_filter_chain:

Network Filter Chain
====================

As discussed in the :ref:`listener <arch_overview_listeners>` section, network level (L3/L4) filters
form the core of Envoy connection handling.

The network filters are chained in a ordered list known as :ref:`filter chain <envoy_v3_api_msg_config.listener.v3.FilterChain>`.
Each listener has multiple filter chains and an optional :ref:`default filter chain <envoy_v3_api_field_config.listener.v3.Listener.default_filter_chain>`.
associated with each filter chain. If the best match filter chain cannot be found, the default filter chain will be
chosen to serve the request. If the default filter chain is not supplied, the connection will be closed.

.. _filter_chain_only_update:

Filter chain only update
------------------------

:ref:`Filter chains <envoy_v3_api_msg_config.listener.v3.FilterChain>` can be updated indepedently. Upon listener config
update, if the listener manager determines that the listener update is a filter chain only update, the listener update
will be executed by adding, updating and removing filter chains. The connections owned by these destroying filter chains will
be drained as described in listener drain.

If the new :ref:`filter chain <envoy_v3_api_msg_config.listener.v3.FilterChain>` and the old :ref:`filter chain <envoy_v3_api_msg_config.listener.v3.FilterChain>`
is protobuf message equivalent, the corresponding filter chain runtime info survives. The connections owned by the
survived filter chains remain open.

Not all the listener config updates can be executed by filter chain update. For example, if the listener metadata is
updated within the new listener config, the new metadata must be picked up by the new filter chains. In this case, the
entire listener is drained and updated.
6 changes: 5 additions & 1 deletion generated_api_shadow/envoy/config/listener/v3/listener.proto

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

#include "extensions/transport_sockets/well_known_names.h"

#include "absl/strings/str_join.h"
#include "openssl/ssl.h"

namespace Envoy {
Expand Down Expand Up @@ -138,6 +139,7 @@ void Filter::onALPN(const unsigned char* data, unsigned int len) {
}
protocols.emplace_back(reinterpret_cast<const char*>(CBS_data(&name)), CBS_len(&name));
}
ENVOY_LOG(trace, "tls:onALPN(), ALPN: {}", absl::StrJoin(protocols, ","));
cb_->socket().setRequestedApplicationProtocols(protocols);
alpn_found_ = true;
}
Expand Down
63 changes: 54 additions & 9 deletions source/server/filter_chain_manager_impl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -141,8 +141,9 @@ bool FilterChainManagerImpl::isWildcardServerName(const std::string& name) {
return absl::StartsWith(name, "*.");
}

void FilterChainManagerImpl::addFilterChain(
void FilterChainManagerImpl::addFilterChains(
absl::Span<const envoy::config::listener::v3::FilterChain* const> filter_chain_span,
const envoy::config::listener::v3::FilterChain* default_filter_chain,
FilterChainFactoryBuilder& filter_chain_factory_builder,
FilterChainFactoryContextCreator& context_creator) {
Cleanup cleanup([this]() { origin_ = absl::nullopt; });
Expand Down Expand Up @@ -183,8 +184,7 @@ void FilterChainManagerImpl::addFilterChain(

// Reject partial wildcards, we don't match on them.
for (const auto& server_name : filter_chain_match.server_names()) {
if (server_name.find('*') != std::string::npos &&
!FilterChainManagerImpl::isWildcardServerName(server_name)) {
if (server_name.find('*') != std::string::npos && !isWildcardServerName(server_name)) {
throw EnvoyException(
fmt::format("error adding listener '{}': partial wildcards are not supported in "
"\"server_names\"",
Expand All @@ -208,13 +208,49 @@ void FilterChainManagerImpl::addFilterChain(
filter_chain_match.server_names(), filter_chain_match.transport_protocol(),
filter_chain_match.application_protocols(), filter_chain_match.source_type(), source_ips,
filter_chain_match.source_ports(), filter_chain_impl);

fc_contexts_[*filter_chain] = filter_chain_impl;
}
convertIPsToTries();
copyOrRebuildDefaultFilterChain(default_filter_chain, filter_chain_factory_builder,
context_creator);
ENVOY_LOG(debug, "new fc_contexts has {} filter chains, including {} newly built",
fc_contexts_.size(), new_filter_chain_size);
}

void FilterChainManagerImpl::copyOrRebuildDefaultFilterChain(
const envoy::config::listener::v3::FilterChain* default_filter_chain,
FilterChainFactoryBuilder& filter_chain_factory_builder,
FilterChainFactoryContextCreator& context_creator) {
// Default filter chain is built exactly once.
ASSERT(!default_filter_chain_message_.has_value());

// Save the default filter chain message. This message could be used in next listener update.
if (default_filter_chain == nullptr) {
return;
}
default_filter_chain_message_ = absl::make_optional(*default_filter_chain);

// Origin filter chain manager could be empty if the current is the ancestor.
const auto* origin = getOriginFilterChainManager();
if (origin == nullptr) {
default_filter_chain_ =
filter_chain_factory_builder.buildFilterChain(*default_filter_chain, context_creator);
return;
}

// Copy from original filter chain manager, or build new filter chain if the default filter chain
// is not equivalent to the one in the original filter chain manager.
MessageUtil eq;
if (origin->default_filter_chain_message_.has_value() &&
eq(origin->default_filter_chain_message_.value(), *default_filter_chain)) {
default_filter_chain_ = origin->default_filter_chain_;
} else {
default_filter_chain_ =
filter_chain_factory_builder.buildFilterChain(*default_filter_chain, context_creator);
}
}

void FilterChainManagerImpl::addFilterChainForDestinationPorts(
DestinationPortsMap& destination_ports_map, uint16_t destination_port,
const std::vector<std::string>& destination_ips,
Expand Down Expand Up @@ -381,21 +417,30 @@ const Network::FilterChain*
FilterChainManagerImpl::findFilterChain(const Network::ConnectionSocket& socket) const {
const auto& address = socket.localAddress();

const Network::FilterChain* best_match_filter_chain = nullptr;
// Match on destination port (only for IP addresses).
if (address->type() == Network::Address::Type::Ip) {
const auto port_match = destination_ports_map_.find(address->ip()->port());
if (port_match != destination_ports_map_.end()) {
return findFilterChainForDestinationIP(*port_match->second.second, socket);
best_match_filter_chain = findFilterChainForDestinationIP(*port_match->second.second, socket);
if (best_match_filter_chain != nullptr) {
return best_match_filter_chain;
} else {
// There is entry for specific port but none of the filter chain matches. Instead of
// matching catch-all port 0, the fallback filter chain is returned.
return default_filter_chain_.get();
}
}
}

// Match on catch-all port 0.
// Match on catch-all port 0 if there is no specific port sub tree.
const auto port_match = destination_ports_map_.find(0);
if (port_match != destination_ports_map_.end()) {
return findFilterChainForDestinationIP(*port_match->second.second, socket);
best_match_filter_chain = findFilterChainForDestinationIP(*port_match->second.second, socket);
}

return nullptr;
return best_match_filter_chain != nullptr
? best_match_filter_chain
// Neither exact port nor catch-all port matches. Use fallback filter chain.
: default_filter_chain_.get();
}

const Network::FilterChain* FilterChainManagerImpl::findFilterChainForDestinationIP(
Expand Down
29 changes: 27 additions & 2 deletions source/server/filter_chain_manager_impl.h
Original file line number Diff line number Diff line change
Expand Up @@ -186,17 +186,36 @@ class FilterChainManagerImpl : public Network::FilterChainManager,

// Add all filter chains into this manager. During the lifetime of FilterChainManagerImpl this
// should be called at most once.
void addFilterChain(
void addFilterChains(
absl::Span<const envoy::config::listener::v3::FilterChain* const> filter_chain_span,
FilterChainFactoryBuilder& b, FilterChainFactoryContextCreator& context_creator);
const envoy::config::listener::v3::FilterChain* default_filter_chain,
FilterChainFactoryBuilder& filter_chain_factory_builder,
FilterChainFactoryContextCreator& context_creator);

static bool isWildcardServerName(const std::string& name);

// Return the current view of filter chains, keyed by filter chain message. Used by the owning
// listener to calculate the intersection of filter chains with another listener.
const FcContextMap& filterChainsByMessage() const { return fc_contexts_; }
const absl::optional<envoy::config::listener::v3::FilterChain>&
defaultFilterChainMessage() const {
return default_filter_chain_message_;
}
const Network::DrainableFilterChainSharedPtr& defaultFilterChain() const {
return default_filter_chain_;
}

private:
void convertIPsToTries();

// Build default filter chain from filter chain message. Skip the build but copy from original
// filter chain manager if the default filter chain message duplicates the message in origin
// filter chain manager. Called by addFilterChains().
void copyOrRebuildDefaultFilterChain(
const envoy::config::listener::v3::FilterChain* default_filter_chain,
FilterChainFactoryBuilder& filter_chain_factory_builder,
FilterChainFactoryContextCreator& context_creator);

using SourcePortsMap = absl::flat_hash_map<uint16_t, Network::FilterChainSharedPtr>;
using SourcePortsMapSharedPtr = std::shared_ptr<SourcePortsMap>;
using SourceIPsMap = absl::flat_hash_map<std::string, SourcePortsMapSharedPtr>;
Expand Down Expand Up @@ -293,9 +312,15 @@ class FilterChainManagerImpl : public Network::FilterChainManager,
// detect the filter chains in the intersection of existing listener and new listener.
FcContextMap fc_contexts_;

absl::optional<envoy::config::listener::v3::FilterChain> default_filter_chain_message_;
// The optional fallback filter chain if destination_ports_map_ does not find a matched filter
// chain.
Network::DrainableFilterChainSharedPtr default_filter_chain_;

// Mapping of FilterChain's configured destination ports, IPs, server names, transport protocols
// and application protocols, using structures defined above.
DestinationPortsMap destination_ports_map_;

const Network::Address::InstanceConstSharedPtr address_;
// This is the reference to a factory context which all the generations of listener share.
Configuration::FactoryContext& parent_context_;
Expand Down
Loading

0 comments on commit 14a39e3

Please sign in to comment.