Skip to content

Commit

Permalink
listener filter: new listener filter for inspecting http protocol (en…
Browse files Browse the repository at this point in the history
…voyproxy#7559)

Description: new listener filter for inspecting http protocol. 
  - Http1x: check request line
  - Http2: check connection preface
    
Pros: Performance; Cons:  False positive possibility

Risk Level: low
Testing: unit test, manual test
Docs Changes: Added
Release Notes: Added
#Issue: envoyproxy#7527

Signed-off-by: Yan Xue <[email protected]>
  • Loading branch information
yxue authored and lizan committed Jul 19, 2019
1 parent 5010947 commit c2967a8
Show file tree
Hide file tree
Showing 14 changed files with 739 additions and 6 deletions.
2 changes: 2 additions & 0 deletions CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,5 @@ extensions/filters/common/original_src @snowp @klarose
/*/extensions/filters/http/dynamic_forward_proxy @mattklein123 @alyssawilk
# omit_canary_hosts retry predicate
/*/extensions/retry/host/omit_canary_hosts @sriduth @snowp
# http inspector
/*/extensions/filters/listener/http_inspector @crazyxy @PiotrSikora @lizan
38 changes: 38 additions & 0 deletions docs/root/configuration/listener_filters/http_inspector.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
.. _config_listener_filters_http_inspector:

HTTP Inspector
==============

HTTP Inspector listener filter allows detecting whether the application protocol appears to be HTTP,
and if it is HTTP, it detects the HTTP protocol (HTTP/1.x or HTTP/2) further. This can be used to select a
:ref:`FilterChain <envoy_api_msg_listener.FilterChain>` via the :ref:`application_protocols <envoy_api_field_listener.FilterChainMatch.application_protocols>`
of a :ref:`FilterChainMatch <envoy_api_msg_listener.FilterChainMatch>`.

* :ref:`v2 API reference <envoy_api_field_listener.ListenerFilter.name>`
* This filter should be configured with the name *envoy.listener.http_inspector*.

Example
-------

A sample filter configuration could be:

.. code-block:: yaml
listener_filters:
- name: "envoy.listener.http_inspector"
typed_config: {}
Statistics
----------

This filter has a statistics tree rooted at *http_inspector* with the following statistics:

.. csv-table::
:header: Name, Type, Description
:widths: 1, 1, 2

read_error, Counter, Total read errors
http10_found, Counter, Total number of times HTTP/1.0 was found
http11_found, Counter, Total number of times HTTP/1.1 was found
http2_found, Counter, Total number of times HTTP/2 was found
http_not_found, Counter, Total number of times HTTP protocol was not found
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ Envoy has the following builtin listener filters.
.. toctree::
:maxdepth: 2

http_inspector
original_dst_filter
original_src_filter
proxy_protocol
Expand Down
1 change: 1 addition & 0 deletions docs/root/intro/version_history.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ Version history
================
* admin: added ability to configure listener :ref:`socket options <envoy_api_field_config.bootstrap.v2.Admin.socket_options>`.
* config: async data access for local and remote data source.
* listeners: added :ref:`HTTP inspector listener filter <config_listener_filters_http_inspector>`.
* http: added the ability to reject HTTP/1.1 requests with invalid HTTP header values, using the runtime feature `envoy.reloadable_features.strict_header_validation`.

1.11.0 (July 11, 2019)
Expand Down
10 changes: 4 additions & 6 deletions source/extensions/extensions_build_config.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -57,16 +57,14 @@ EXTENSIONS = {
# Listener filters
#

# NOTE: The proxy_protocol filter is implicitly loaded if proxy_protocol functionality is
# configured on the listener. Do not remove it in that case or configs will fail to load.
"envoy.filters.listener.proxy_protocol": "//source/extensions/filters/listener/proxy_protocol:config",

"envoy.filters.listener.http_inspector": "//source/extensions/filters/listener/http_inspector:config",
# NOTE: The original_dst filter is implicitly loaded if original_dst functionality is
# configured on the listener. Do not remove it in that case or configs will fail to load.
"envoy.filters.listener.original_dst": "//source/extensions/filters/listener/original_dst:config",

"envoy.filters.listener.original_src": "//source/extensions/filters/listener/original_src:config",

# NOTE: The proxy_protocol filter is implicitly loaded if proxy_protocol functionality is
# configured on the listener. Do not remove it in that case or configs will fail to load.
"envoy.filters.listener.proxy_protocol": "//source/extensions/filters/listener/proxy_protocol:config",
"envoy.filters.listener.tls_inspector": "//source/extensions/filters/listener/tls_inspector:config",

#
Expand Down
38 changes: 38 additions & 0 deletions source/extensions/filters/listener/http_inspector/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
licenses(["notice"]) # Apache 2

# HTTP inspector filter for sniffing HTTP protocol and setting HTTP version to a FilterChain.

load(
"//bazel:envoy_build_system.bzl",
"envoy_cc_library",
"envoy_package",
)

envoy_package()

envoy_cc_library(
name = "http_inspector_lib",
srcs = ["http_inspector.cc"],
hdrs = ["http_inspector.h"],
deps = [
"//include/envoy/event:dispatcher_interface",
"//include/envoy/event:timer_interface",
"//include/envoy/network:filter_interface",
"//include/envoy/network:listen_socket_interface",
"//source/common/api:os_sys_calls_lib",
"//source/common/common:minimal_logger_lib",
"//source/common/http:headers_lib",
"//source/extensions/transport_sockets:well_known_names",
],
)

envoy_cc_library(
name = "config",
srcs = ["config.cc"],
deps = [
":http_inspector_lib",
"//include/envoy/registry",
"//include/envoy/server:filter_config_interface",
"//source/extensions/filters/listener:well_known_names",
],
)
43 changes: 43 additions & 0 deletions source/extensions/filters/listener/http_inspector/config.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
#include "envoy/registry/registry.h"
#include "envoy/server/filter_config.h"

#include "extensions/filters/listener/http_inspector/http_inspector.h"
#include "extensions/filters/listener/well_known_names.h"

namespace Envoy {
namespace Extensions {
namespace ListenerFilters {
namespace HttpInspector {

/**
* Config registration for the Http inspector filter. @see NamedNetworkFilterConfigFactory.
*/
class HttpInspectorConfigFactory : public Server::Configuration::NamedListenerFilterConfigFactory {
public:
// NamedListenerFilterConfigFactory
Network::ListenerFilterFactoryCb
createFilterFactoryFromProto(const Protobuf::Message&,
Server::Configuration::ListenerFactoryContext& context) override {
ConfigSharedPtr config(std::make_shared<Config>(context.scope()));
return [config](Network::ListenerFilterManager& filter_manager) -> void {
filter_manager.addAcceptFilter(std::make_unique<Filter>(config));
};
}

ProtobufTypes::MessagePtr createEmptyConfigProto() override {
return std::make_unique<Envoy::ProtobufWkt::Empty>();
}

std::string name() override { return ListenerFilterNames::get().HttpInspector; }
};

/**
* Static registration for the http inspector filter. @see RegisterFactory.
*/
REGISTER_FACTORY(HttpInspectorConfigFactory,
Server::Configuration::NamedListenerFilterConfigFactory);

} // namespace HttpInspector
} // namespace ListenerFilters
} // namespace Extensions
} // namespace Envoy
145 changes: 145 additions & 0 deletions source/extensions/filters/listener/http_inspector/http_inspector.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
#include "extensions/filters/listener/http_inspector/http_inspector.h"

#include "envoy/event/dispatcher.h"
#include "envoy/network/listen_socket.h"
#include "envoy/stats/scope.h"

#include "common/api/os_sys_calls_impl.h"
#include "common/common/assert.h"
#include "common/common/macros.h"
#include "common/http/headers.h"

#include "extensions/transport_sockets/well_known_names.h"

#include "absl/strings/match.h"
#include "absl/strings/str_split.h"

namespace Envoy {
namespace Extensions {
namespace ListenerFilters {
namespace HttpInspector {

Config::Config(Stats::Scope& scope)
: stats_{ALL_HTTP_INSPECTOR_STATS(POOL_COUNTER_PREFIX(scope, "http_inspector."))} {}

const absl::string_view Filter::HTTP2_CONNECTION_PREFACE = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n";
thread_local uint8_t Filter::buf_[Config::MAX_INSPECT_SIZE];

Filter::Filter(const ConfigSharedPtr config) : config_(config) {}

Network::FilterStatus Filter::onAccept(Network::ListenerFilterCallbacks& cb) {
ENVOY_LOG(debug, "http inspector: new connection accepted");

Network::ConnectionSocket& socket = cb.socket();

absl::string_view transport_protocol = socket.detectedTransportProtocol();
if (!transport_protocol.empty() &&
transport_protocol != TransportSockets::TransportSocketNames::get().RawBuffer) {
ENVOY_LOG(trace, "http inspector: cannot inspect http protocol with transport socket {}",
transport_protocol);
return Network::FilterStatus::Continue;
}

ASSERT(file_event_ == nullptr);

file_event_ = cb.dispatcher().createFileEvent(
socket.ioHandle().fd(),
[this](uint32_t events) {
ASSERT(events == Event::FileReadyType::Read);
onRead();
},
Event::FileTriggerType::Edge, Event::FileReadyType::Read);

cb_ = &cb;
return Network::FilterStatus::StopIteration;
}

void Filter::onRead() {
auto& os_syscalls = Api::OsSysCallsSingleton::get();
const Api::SysCallSizeResult result =
os_syscalls.recv(cb_->socket().ioHandle().fd(), buf_, Config::MAX_INSPECT_SIZE, MSG_PEEK);
ENVOY_LOG(trace, "http inspector: recv: {}", result.rc_);
if (result.rc_ == -1 && result.errno_ == EAGAIN) {
return;
} else if (result.rc_ < 0) {
config_->stats().read_error_.inc();
done(false);
return;
}

parseHttpHeader(absl::string_view(reinterpret_cast<const char*>(buf_), result.rc_));
}

void Filter::parseHttpHeader(absl::string_view data) {
if (absl::StartsWith(data, Filter::HTTP2_CONNECTION_PREFACE)) {
ENVOY_LOG(trace, "http inspector: http2 connection preface found");
protocol_ = "HTTP/2";
done(true);
} else {
size_t pos = data.find_first_of("\r\n");

// Cannot find \r or \n
if (pos == absl::string_view::npos) {
ENVOY_LOG(trace, "http inspector: no request line detected");
return done(false);
}

absl::string_view request_line = data.substr(0, pos);
std::vector<absl::string_view> fields = absl::StrSplit(request_line, absl::MaxSplits(' ', 4));

// Method SP Request-URI SP HTTP-Version
if (fields.size() != 3) {
ENVOY_LOG(trace, "http inspector: invalid http1x request line");
return done(false);
}

if (httpProtocols().count(fields[2]) == 0) {
ENVOY_LOG(trace, "http inspector: protocol: {} not valid", fields[2]);
return done(false);
}

ENVOY_LOG(trace, "http inspector: method: {}, request uri: {}, protocol: {}", fields[0],
fields[1], fields[2]);

protocol_ = fields[2];
done(true);
}
}

void Filter::done(bool success) {
ENVOY_LOG(trace, "http inspector: done: {}", success);

if (success) {
absl::string_view protocol;
if (protocol_ == Http::Headers::get().ProtocolStrings.Http10String) {
config_->stats().http10_found_.inc();
protocol = "http/1.0";
} else if (protocol_ == Http::Headers::get().ProtocolStrings.Http11String) {
config_->stats().http11_found_.inc();
protocol = "http/1.1";
} else {
ASSERT(protocol_ == "HTTP/2");
config_->stats().http2_found_.inc();
protocol = "h2";
}

cb_->socket().setRequestedApplicationProtocols({protocol});
} else {
config_->stats().http_not_found_.inc();
}

file_event_.reset();
// Do not skip following listener filters.
cb_->continueFilterChain(true);
}

const absl::flat_hash_set<std::string>& Filter::httpProtocols() const {
CONSTRUCT_ON_FIRST_USE(absl::flat_hash_set<std::string>,
Http::Headers::get().ProtocolStrings.Http10String,
Http::Headers::get().ProtocolStrings.Http11String);
}

} // namespace HttpInspector
} // namespace ListenerFilters
} // namespace Extensions
} // namespace Envoy
83 changes: 83 additions & 0 deletions source/extensions/filters/listener/http_inspector/http_inspector.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
#pragma once

#include "envoy/event/file_event.h"
#include "envoy/event/timer.h"
#include "envoy/network/filter.h"
#include "envoy/stats/scope.h"
#include "envoy/stats/stats_macros.h"

#include "common/common/logger.h"

#include "absl/container/flat_hash_set.h"

namespace Envoy {
namespace Extensions {
namespace ListenerFilters {
namespace HttpInspector {

/**
* All stats for the http inspector. @see stats_macros.h
*/
#define ALL_HTTP_INSPECTOR_STATS(COUNTER) \
COUNTER(read_error) \
COUNTER(http10_found) \
COUNTER(http11_found) \
COUNTER(http2_found) \
COUNTER(http_not_found)

/**
* Definition of all stats for the Http inspector. @see stats_macros.h
*/
struct HttpInspectorStats {
ALL_HTTP_INSPECTOR_STATS(GENERATE_COUNTER_STRUCT)
};

/**
* Global configuration for http inspector.
*/
class Config {
public:
Config(Stats::Scope& scope);

const HttpInspectorStats& stats() const { return stats_; }

static constexpr uint32_t MAX_INSPECT_SIZE = 8192;

private:
HttpInspectorStats stats_;
};

using ConfigSharedPtr = std::shared_ptr<Config>;

/**
* Http inspector listener filter.
*/
class Filter : public Network::ListenerFilter, Logger::Loggable<Logger::Id::filter> {
public:
Filter(const ConfigSharedPtr config);

// Network::ListenerFilter
Network::FilterStatus onAccept(Network::ListenerFilterCallbacks& cb) override;

private:
static const absl::string_view HTTP2_CONNECTION_PREFACE;

void onRead();
void done(bool success);
void parseHttpHeader(absl::string_view data);

const absl::flat_hash_set<std::string>& httpProtocols() const;

ConfigSharedPtr config_;
Network::ListenerFilterCallbacks* cb_{nullptr};
Event::FileEventPtr file_event_;
absl::string_view protocol_;

// Use static thread_local to avoid allocating buffer over and over again.
static thread_local uint8_t buf_[Config::MAX_INSPECT_SIZE];
};

} // namespace HttpInspector
} // namespace ListenerFilters
} // namespace Extensions
} // namespace Envoy
Loading

0 comments on commit c2967a8

Please sign in to comment.