Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add routing capabilities to tcp_proxy #377

Merged
merged 31 commits into from
Feb 1, 2017
Merged
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
09dd326
Extend and customize listener behavior
Jan 5, 2017
ed44be2
Incorporate review comments (except ConnectionHandler interface)
Jan 9, 2017
60e72a6
Create an interface for ConnectionHandler
Jan 10, 2017
2ff9595
Merge remote-tracking branch 'upstream/master'
Jan 10, 2017
14eaed4
Fix style and typo
Jan 10, 2017
f655994
Add regular expression support in header match (#335)
rshriram Jan 10, 2017
8641d2a
Replace openssl with boringssl as the official ssl provider (#339)
RomanDzhabarov Jan 10, 2017
aa20a22
stats: fix scope timer prefix (#340)
mattklein123 Jan 10, 2017
fa5bbd9
Move ConnectionHandler interface to namespace Network and incorporate…
Jan 10, 2017
fa5aa3c
Merge remote-tracking branch 'upstream/master'
Jan 11, 2017
987bc7a
Fix bad merge
Jan 11, 2017
d1c75f6
Incorporate review comment
Jan 12, 2017
7315ce3
Merge remote-tracking branch 'upstream/master'
Jan 12, 2017
8e746bf
Merge remote-tracking branch 'upstream/master'
Jan 23, 2017
5e2c2b1
Fix handling of subnet "0.0.0.0/0" in whitelist.
Jan 23, 2017
becb250
Add a comment regarding the fix for "0.0.0.0/0" subnet
Jan 23, 2017
2729237
Add a comment regarding the fix for "0.0.0.0/0" subnet
Jan 23, 2017
d4b80ac
Merge branch 'master' of https://github.com/enricoschiattarella/envoy
Jan 23, 2017
d833ec8
Add routing capabilities to tcp_proxy
Jan 25, 2017
d877b8a
Add some comments and cosmetic fixes.
Jan 25, 2017
0b1cc69
Merge remote-tracking branch 'upstream/master'
Jan 25, 2017
91bd2a6
Add missing file
Jan 25, 2017
8b53016
Incorporate review comments
Jan 26, 2017
5e6fbc4
Merge remote-tracking branch 'upstream/master'
Jan 27, 2017
8bc1d82
Fix TCP_PROXY_NETWORK_FILTER_SCHEMA error.
Jan 27, 2017
2dffce8
Chande destinationAddress() to localAddress() and populate at constru…
Jan 27, 2017
6eecce2
Make local_addr part of connection and pass it down from listeners
Jan 28, 2017
435ac47
TCP proxy routing: config validation logic, test for non-routable con…
enricoschiattarella Jan 31, 2017
5e46ef9
Merge remote-tracking branch 'upstream/master'
enricoschiattarella Jan 31, 2017
1548bce
Merge remote-tracking branch 'upstream/master'
enricoschiattarella Feb 1, 2017
87d9a90
Fix include order
enricoschiattarella Feb 1, 2017
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions configs/envoy_service_to_service.template.json
Original file line number Diff line number Diff line change
Expand Up @@ -245,8 +245,14 @@
"type": "read",
"name": "tcp_proxy",
"config": {
"cluster": "mongo_{{ key }}",
"stat_prefix": "mongo_{{ key }}"
"stat_prefix": "mongo_{{ key }}",
"route_config": {
"routes": [
{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i'd probably put here those optional config settings here as well, so one checking this template file can quickly get a sense of all options.
will leave up to you.
These configs are loaded as part of Envoy tests as well.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, I had the impression that these templates were used for generating actual configs. If they are just loaded during Envoy tests I will add them so that the parsing logic gets exercised.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't find a good real-world example for this because in configgen.py the tcp_proxy instances and the clusters are coupled (since there used to be a 1:1 mapping). Let me think a bit more about this while you guys review the new version.

"cluster": "mongo_{{ key }}"
}
]
}
}
}]
}{% if not loop.last %},{% endif -%}
Expand Down
108 changes: 103 additions & 5 deletions docs/configuration/network_filters/tcp_proxy_filter.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,114 @@ TCP proxy :ref:`architecture overview <arch_overview_tcp_proxy>`.
"type": "read",
"name": "tcp_proxy",
"config": {
"cluster": "...",
"stat_prefix": "..."
"stat_prefix": "...",
"route_config": "{...}"
}
}

cluster
*(required, string)* The :ref:`cluster manager <arch_overview_cluster_manager>` cluster to connect
to when a new downstream network connection is received.
:ref:`route_config <config_network_filters_tcp_proxy_route_config>`
*(required, object)* The route table for the filter.
All filter instances must have a route table, even if it is empty.

stat_prefix
*(required, string)* The prefix to use when emitting :ref:`statistics
<config_network_filters_tcp_proxy_stats>`.

.. _config_network_filters_tcp_proxy_route_config:

Route Configuration
-------------------

.. code-block:: json

{
"routes": []
}

:ref:`routes <config_network_filters_tcp_proxy_route>`
*(required, array)* An array of route entries that make up the route table.

.. _config_network_filters_tcp_proxy_route:

Route
-----

A TCP proxy route consists of a set of optional L4 criteria and the name of a
:ref:`cluster <config_cluster_manager_cluster>`. If a downstream connection matches
all the specified criteria, the cluster in the route is used for the corresponding upstream
connection. Routes are tried in the order specified until a match is found. If no match is
found, the connection is closed. A route with no criteria is valid and always produces a match.

.. code-block:: json

{
"cluster": "...",
"destination_ip_list": [],
"destination_ports": "...",
"source_ip_list": [],
"source_ports": "..."
}

cluster
*(required, string)* The :ref:`cluster <config_cluster_manager_cluster>` to connect
to when a the downstream network connection matches the specified criteria.

destination_ip_list
*(optional, array)* An optional list of IPv4 subnets in the form "a.b.c.d/xx".
The criteria is satisfied if the destination IP address of the downstream connection is
contained in at least one of the specified subnets.
If the parameter is not specified or the list is empty, the destination IP address is ignored.
The destination IP address of the downstream connection might be different from the addresses
on which the proxy is listening if the connection has been redirected. Example:

.. code-block:: json

[
"192.168.3.0/24",
"50.1.2.3/32",
"10.15.0.0/16"
]

destination_ports
*(optional, string)* An optional string containing a comma-separated list of port numbers or
ranges. The criteria is satisfied if the destination port of the downstream connection
is contained in at least one of the specified ranges.
If the parameter is not specified or the list is empty, the destination port is ignored.
The destination port address of the downstream connection might be different from the port
on which the proxy is listening if the connection has been redirected. Example:

.. code-block:: json

{
"destination_ports": "1-1024,2048-4096,12345"
}

source_ip_list
*(optional, array)* An optional list of IPv4 subnets in the form "a.b.c.d/xx".
The criteria is satisfied if the source IP address of the downstream connection is contained
in at least one of the specified subnets. If the parameter is not specified or the list is empty,
the source IP address is ignored. Example:

.. code-block:: json

[
"192.168.3.0/24",
"50.1.2.3/32",
"10.15.0.0/16"
]

source_ports
*(optional, string)* An optional string containing a comma-separated list of port numbers or
ranges. The criteria is satisfied if the source port of the downstream connection is contained
in at least one of the specified ranges. If the parameter is not specified or the list is empty,
the source port is ignored. Example:

.. code-block:: json

{
"source_ports": "1-1024,2048-4096,12345"
}

.. _config_network_filters_tcp_proxy_stats:

Statistics
Expand All @@ -37,5 +132,8 @@ statistics are rooted at *tcp.<stat_prefix>.* with the following statistics:
:header: Name, Type, Description
:widths: 1, 1, 2

downstream_cx_total, Counter, Total number of connections handled by the filter.
downstream_cx_no_route, Counter, Number of connections for which no matching route was found.
downstream_cx_tx_bytes_total, Counter, Total bytes written to the downstream connection.
downstream_cx_tx_bytes_buffered, Gauge, Total bytes currently buffered to the downstream connection.

11 changes: 10 additions & 1 deletion include/envoy/network/connection.h
Original file line number Diff line number Diff line change
Expand Up @@ -111,10 +111,19 @@ class Connection : public Event::DeferredDeletable, public FilterManager {
virtual bool readEnabled() PURE;

/**
* @return The address of the remote client
* @return The address of the remote client.
* For TCP connections, it is in the form tcp://a.b.c.d:port
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it might not be of the of the form tcp:// if it's a UDS connection. I would just delete that part of the comment for now. I opened #390 to clean this up and will get to this soon.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

*/
virtual const std::string& remoteAddress() PURE;

/**
* @return the local address of the connection. For client connections, this is the origin
* address. For server connections, this is the local destination address. For server connections
* it can be different from the proxy address if the downstream connection has been redirected or
* the proxy is operating in transparent mode.
*/
virtual const std::string& localAddress() PURE;

/**
* Set the buffer stats to update when the connection's read/write buffers change. Note that
* for performance reasons these stats are eventually consistent and may not always accurately
Expand Down
4 changes: 3 additions & 1 deletion source/common/filter/auth/client_ssl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#include "common/http/message_impl.h"
#include "common/http/utility.h"
#include "common/json/config_schemas.h"
#include "common/network/utility.h"

namespace Filter {
namespace Auth {
Expand Down Expand Up @@ -97,7 +98,8 @@ void Instance::onEvent(uint32_t events) {
}

ASSERT(read_callbacks_->connection().ssl());
if (config_->ipWhiteList().contains(read_callbacks_->connection().remoteAddress())) {
if (config_->ipWhiteList().contains(
Network::Utility::hostFromUrl(read_callbacks_->connection().remoteAddress()))) {
config_->stats().auth_ip_white_list_.inc();
read_callbacks_->continueReading();
return;
Expand Down
95 changes: 87 additions & 8 deletions source/common/filter/tcp_proxy.cc
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,89 @@

#include "common/common/assert.h"
#include "common/json/config_schemas.h"
#include "common/common/empty_string.h"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: alpha order

#include "common/json/json_loader.h"

namespace Filter {

TcpProxyConfig::Route::Route(const Json::Object& config) {
cluster_name_ = config.getString("cluster");

if (config.hasObject("source_ip_list")) {
source_ips_ = Network::IpList(config.getStringArray("source_ip_list"));
}

if (config.hasObject("source_ports")) {
Network::Utility::parsePortRangeList(config.getString("source_ports"), source_port_ranges_);
}

if (config.hasObject("destination_ip_list")) {
destination_ips_ = Network::IpList(config.getStringArray("destination_ip_list"));
}

if (config.hasObject("destination_ports")) {
Network::Utility::parsePortRangeList(config.getString("destination_ports"),
destination_port_ranges_);
}
}

TcpProxyConfig::TcpProxyConfig(const Json::Object& config,
Upstream::ClusterManager& cluster_manager, Stats::Store& stats_store)
: cluster_name_(config.getString("cluster")),
stats_(generateStats(config.getString("stat_prefix"), stats_store)) {

: stats_(generateStats(config.getString("stat_prefix"), stats_store)) {
config.validateSchema(Json::Schema::TCP_PROXY_NETWORK_FILTER_SCHEMA);

if (!cluster_manager.get(cluster_name_)) {
throw EnvoyException(fmt::format("tcp proxy: unknown cluster '{}'", cluster_name_));
for (const Json::ObjectPtr& route_desc :
config.getObject("route_config")->getObjectArray("routes")) {
routes_.emplace_back(Route(*route_desc));

if (!cluster_manager.get(route_desc->getString("cluster"))) {
throw EnvoyException(fmt::format("tcp proxy: unknown cluster '{}' in TCP route",
route_desc->getString("cluster")));
}
}
}

const std::string& TcpProxyConfig::getRouteFromEntries(Network::Connection& connection) {
for (const TcpProxyConfig::Route& route : routes_) {
if (!route.source_port_ranges_.empty() &&
!Network::Utility::portInRangeList(
Network::Utility::portFromUrl(connection.remoteAddress()), route.source_port_ranges_)) {
continue; // no match, try next route
}

if (!route.source_ips_.empty() &&
!route.source_ips_.contains(Network::Utility::hostFromUrl(connection.remoteAddress()))) {
continue; // no match, try next route
}

// If the route needs to match on destination address and port but they are not available
// (localAddress is empty), we skip it. The connection has a chance to match a different
// route that does not depend on destination address and port.
if ((!route.destination_port_ranges_.empty() || !route.destination_ips_.empty()) &&
connection.localAddress().empty()) {
continue;
}

if (!route.destination_port_ranges_.empty() &&
!Network::Utility::portInRangeList(Network::Utility::portFromUrl(connection.localAddress()),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if localAddress() is empty, this will throw an exception, so I think for now need to check if it's not empty.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok

route.destination_port_ranges_)) {
continue; // no match, try next route
}

if (!route.destination_ips_.empty() &&
!route.destination_ips_.contains(
Network::Utility::hostFromUrl(connection.localAddress()))) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if localAddress() is empty, this will throw an exception, so I think for now need to check if it's not empty.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok

continue; // no match, try next route
}

// if we made it past all checks, the route matches
return route.cluster_name_;
}

// no match, no more routes to try
return EMPTY_STRING;
}

TcpProxy::TcpProxy(TcpProxyConfigPtr config, Upstream::ClusterManager& cluster_manager)
: config_(config), cluster_manager_(cluster_manager), downstream_callbacks_(*this),
upstream_callbacks_(new UpstreamCallbacks(*this)) {}
Expand Down Expand Up @@ -52,6 +119,7 @@ TcpProxyStats TcpProxyConfig::generateStats(const std::string& name, Stats::Stor
void TcpProxy::initializeReadFilterCallbacks(Network::ReadFilterCallbacks& callbacks) {
read_callbacks_ = &callbacks;
conn_log_info("new tcp proxy session", read_callbacks_->connection());
config_->stats().downstream_cx_total_.inc();
read_callbacks_->connection().addConnectionCallbacks(downstream_callbacks_);
read_callbacks_->connection().setBufferStats({config_->stats().downstream_cx_rx_bytes_total_,
config_->stats().downstream_cx_rx_bytes_buffered_,
Expand All @@ -60,14 +128,25 @@ void TcpProxy::initializeReadFilterCallbacks(Network::ReadFilterCallbacks& callb
}

Network::FilterStatus TcpProxy::initializeUpstreamConnection() {
Upstream::ClusterInfoPtr cluster = cluster_manager_.get(config_->clusterName());
const std::string& cluster_name = config_->getRouteFromEntries(read_callbacks_->connection());

Upstream::ClusterInfoPtr cluster = cluster_manager_.get(cluster_name);

if (cluster) {
conn_log_debug("Creating connection to cluster {}", read_callbacks_->connection(),
cluster_name);
} else {
config_->stats().downstream_cx_no_route_.inc();
read_callbacks_->connection().close(Network::ConnectionCloseType::NoFlush);
return Network::FilterStatus::StopIteration;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would be good to have a stat on this scenario
should we explicitly close connection here as well?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently this can't happen, because you verify that all the clusters exist. If we eventually want to support CDS/RDS like constructs for tcp_proxy, we will need to deal with this, but for now I would not have this logic at all and just crash if cluster is nullptr.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The clusters in the config always exist, but the downstream connection parameters may not match any of the programmed routes (if there's no default), in which case getClusterForConnection() will return "". I think Roman's concern is valid and we should close the connection in that case.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am also adding a counter for total number of connections handled by the filter and number of connections closed due to no route match

}

if (!cluster->resourceManager(Upstream::ResourcePriority::Default).connections().canCreate()) {
cluster->stats().upstream_cx_overflow_.inc();
read_callbacks_->connection().close(Network::ConnectionCloseType::NoFlush);
return Network::FilterStatus::StopIteration;
}
Upstream::Host::CreateConnectionData conn_info =
cluster_manager_.tcpConnForCluster(config_->clusterName());
Upstream::Host::CreateConnectionData conn_info = cluster_manager_.tcpConnForCluster(cluster_name);

upstream_connection_ = std::move(conn_info.connection_);
read_callbacks_->upstreamHost(conn_info.host_description_);
Expand Down
28 changes: 25 additions & 3 deletions source/common/filter/tcp_proxy.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#include "common/common/logger.h"
#include "common/json/json_loader.h"
#include "common/network/filter_impl.h"
#include "common/network/utility.h"

namespace Filter {

Expand All @@ -20,7 +21,9 @@ namespace Filter {
COUNTER(downstream_cx_rx_bytes_total) \
GAUGE (downstream_cx_rx_bytes_buffered) \
COUNTER(downstream_cx_tx_bytes_total) \
GAUGE (downstream_cx_tx_bytes_buffered)
GAUGE (downstream_cx_tx_bytes_buffered) \
COUNTER(downstream_cx_total) \
COUNTER(downstream_cx_no_route)
// clang-format on

/**
Expand All @@ -38,13 +41,32 @@ class TcpProxyConfig {
TcpProxyConfig(const Json::Object& config, Upstream::ClusterManager& cluster_manager,
Stats::Store& stats_store);

const std::string& clusterName() { return cluster_name_; }
/**
* Find out which cluster an upstream connection should be opened to based on the
* parameters of a downstream connection.
* @param connection supplies the parameters of the downstream connection for
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

upstream connection may be, can you fix the comment

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment is actually correct, but maybe not clear. I have clarified.

* which the proxy needs to open the corresponding upstream.
* @return the cluster name to be used for the upstream connection.
* If no route applies, returns the empty string.
*/
const std::string& getRouteFromEntries(Network::Connection& connection);

const TcpProxyStats& stats() { return stats_; }

private:
struct Route {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move out of class, to make it extensible? similar to weighted clusters use case? [it can be done later as well. But a TODO might help keep track of this]. @mattklein123 WDYT? Weighted clusters for tcp could follow a similar implementation like HTTP.

Copy link
Member

@mattklein123 mattklein123 Jan 26, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rshriram I agree that ultimately we will want to make this more complicated, and probably use RDS, etc., but for now in the interest of time I think it's OK to keep this here since it's so simple. We can refactor it out in a future change.

Route(const Json::Object& config);

Network::IpList source_ips_;
Network::PortRangeList source_port_ranges_;
Network::IpList destination_ips_;
Network::PortRangeList destination_port_ranges_;
std::string cluster_name_;
};

static TcpProxyStats generateStats(const std::string& name, Stats::Store& store);

std::string cluster_name_;
std::vector<Route> routes_;
const TcpProxyStats stats_;
};

Expand Down
Loading