diff --git a/DEVELOPER.md b/DEVELOPER.md index 40aafaf3e651c..6050a4095f6f7 100644 --- a/DEVELOPER.md +++ b/DEVELOPER.md @@ -26,7 +26,7 @@ Below is a list of additional documentation to aid the development process: - [Envoy filter example project (how to consume and extend Envoy as a submodule)](https://github.com/envoyproxy/envoy-filter-example) -- [Performance testing Envoy with `tcmalloc`/`pprof`](https://github.com/envoyproxy/envoy/tree/bazel/PPROF.md) +- [Performance testing Envoy with `tcmalloc`/`pprof`](https://github.com/envoyproxy/envoy/blob/master/bazel/PPROF.md) And some documents on components of Envoy architecture: diff --git a/api/envoy/config/rbac/v2alpha/rbac.proto b/api/envoy/config/rbac/v2alpha/rbac.proto index d7431eb0e1544..9c1b04c24d15e 100644 --- a/api/envoy/config/rbac/v2alpha/rbac.proto +++ b/api/envoy/config/rbac/v2alpha/rbac.proto @@ -126,6 +126,27 @@ message Permission { // match, this permission would not match. Conversely, if the value of `not_rule` would not // match, this permission would match. Permission not_rule = 8; + + // The request server from the client's connection request. This is + // typically TLS SNI. + // + // .. attention:: + // + // The behavior of this field may be affected by how Envoy is configured + // as explained below. + // + // * If the :ref:`TLS Inspector ` + // filter is not added, and if a `FilterChainMatch` is not defined for + // the :ref:`server name `, + // a TLS connection's requested SNI server name will be treated as if it + // wasn't present. + // + // * A :ref:`listener filter ` may + // overwrite a connection's requested server name within Envoy. + // + // Please refer to :ref:`this FAQ entry ` to learn to + // setup SNI. + envoy.type.matcher.StringMatcher requested_server_name = 9; } } diff --git a/api/envoy/config/trace/v2/trace.proto b/api/envoy/config/trace/v2/trace.proto index 376948bb3084b..a7c543e8837b4 100644 --- a/api/envoy/config/trace/v2/trace.proto +++ b/api/envoy/config/trace/v2/trace.proto @@ -80,6 +80,14 @@ message DynamicOtConfig { google.protobuf.Struct config = 2; } +// Configuration for the Datadog tracer. +message DatadogConfig { + // The cluster to use for submitting traces to the Datadog agent. + string collector_cluster = 1 [(validate.rules).string.min_bytes = 1]; + string service_name = 2 [(validate.rules).string.min_bytes = 1]; + bool priority_sampling = 3; +} + // Configuration structure. message TraceServiceConfig { // The upstream gRPC cluster that hosts the metrics service. diff --git a/bazel/external/BUILD b/bazel/external/BUILD index 63a81dd5e12b1..f2111ab6b8843 100644 --- a/bazel/external/BUILD +++ b/bazel/external/BUILD @@ -6,6 +6,7 @@ cc_library( deps = [ "@com_google_googletest//:gtest", "@com_lightstep_tracer_cpp//:lightstep_tracer", + "@com_github_datadog_dd_opentracing_cpp//:dd_opentracing_cpp", "@io_opentracing_cpp//:opentracing", ], ) diff --git a/bazel/repositories.bzl b/bazel/repositories.bzl index a62bde82cc821..b594207ff1fce 100644 --- a/bazel/repositories.bzl +++ b/bazel/repositories.bzl @@ -311,6 +311,7 @@ def envoy_dependencies(path = "@envoy_deps//", skip_targets = []): _com_github_google_libprotobuf_mutator() _io_opentracing_cpp() _com_lightstep_tracer_cpp() + _com_github_datadog_dd_opentracing_cpp() _com_github_grpc_grpc() _com_github_google_jwt_verify() _com_github_nanopb_nanopb() @@ -432,6 +433,17 @@ def _com_lightstep_tracer_cpp(): actual = "@com_lightstep_tracer_cpp//:lightstep_tracer", ) +def _com_github_datadog_dd_opentracing_cpp(): + _repository_impl("com_github_datadog_dd_opentracing_cpp") + _repository_impl( + name = "com_github_msgpack_msgpack_c", + build_file = "@com_github_datadog_dd_opentracing_cpp//:bazel/external/msgpack.BUILD", + ) + native.bind( + name = "dd_opentracing_cpp", + actual = "@com_github_datadog_dd_opentracing_cpp//:dd_opentracing_cpp", + ) + def _com_github_tencent_rapidjson(): _repository_impl( name = "com_github_tencent_rapidjson", diff --git a/bazel/repository_locations.bzl b/bazel/repository_locations.bzl index 047f443a738eb..c6645b3e9fc36 100644 --- a/bazel/repository_locations.bzl +++ b/bazel/repository_locations.bzl @@ -82,6 +82,14 @@ REPOSITORY_LOCATIONS = dict( commit = "d6f78d948c53f3b400bb46996eb3084359914f9b", remote = "https://github.com/google/googleapis", ), + com_github_datadog_dd_opentracing_cpp = dict( + commit = "92d7ee11f61361ca23e00b48d328fb4e494534c4", # v0.3.1 + remote = "https://github.com/DataDog/dd-opentracing-cpp", + ), + com_github_msgpack_msgpack_c = dict( + commit = "83a82e3eb512b18d4149cabb7eb43c7e8bc081af", + remote = "https://github.com/msgpack/msgpack-c", # v3.1.1 + ), com_github_google_jwt_verify = dict( commit = "66792a057ec54e4b75c6a2eeda4e98220bd12a9a", # 2018-08-17 remote = "https://github.com/google/jwt_verify_lib", diff --git a/docs/root/intro/version_history.rst b/docs/root/intro/version_history.rst index db57b93ceafb3..4955dbe023c5c 100644 --- a/docs/root/intro/version_history.rst +++ b/docs/root/intro/version_history.rst @@ -10,6 +10,7 @@ Version history * http: no longer adding whitespace when appending X-Forwarded-For headers. **Warning**: this is not compatible with 1.7.0 builds prior to `9d3a4eb4ac44be9f0651fcc7f87ad98c538b01ee `_. See `#3611 `_ for details. +* rbac: added support for permission matching by :ref:`requested server name `. * router: added ability to configure arbitrary :ref:`retriable status codes. ` * router: added ability to set attempt count in upstream requests, see :ref:`virtual host's include request attempt count flag `. diff --git a/source/common/common/matchers.cc b/source/common/common/matchers.cc index 4121b599fa8da..e7e6895eb68f4 100644 --- a/source/common/common/matchers.cc +++ b/source/common/common/matchers.cc @@ -64,7 +64,7 @@ bool StringMatcher::match(const ProtobufWkt::Value& value) const { return match(value.string_value()); } -bool StringMatcher::match(const std::string& value) const { +bool StringMatcher::match(const absl::string_view value) const { switch (matcher_.match_pattern_case()) { case envoy::type::matcher::StringMatcher::kExact: return matcher_.exact() == value; @@ -73,7 +73,7 @@ bool StringMatcher::match(const std::string& value) const { case envoy::type::matcher::StringMatcher::kSuffix: return absl::EndsWith(value, matcher_.suffix()); case envoy::type::matcher::StringMatcher::kRegex: - return std::regex_match(value, regex_); + return std::regex_match(value.begin(), value.end(), regex_); default: NOT_REACHED_GCOVR_EXCL_LINE; } diff --git a/source/common/common/matchers.h b/source/common/common/matchers.h index 1fd4a210517b1..43dcf78ed1306 100644 --- a/source/common/common/matchers.h +++ b/source/common/common/matchers.h @@ -78,7 +78,7 @@ class StringMatcher : public ValueMatcher { } } - bool match(const std::string& value) const; + bool match(const absl::string_view value) const; bool match(const ProtobufWkt::Value& value) const override; diff --git a/source/common/http/conn_manager_impl.cc b/source/common/http/conn_manager_impl.cc index 733c5a4e4535e..a3aaa9976ba68 100644 --- a/source/common/http/conn_manager_impl.cc +++ b/source/common/http/conn_manager_impl.cc @@ -587,10 +587,10 @@ void ConnectionManagerImpl::ActiveStream::decodeHeaders(HeaderMapPtr&& headers, } // Currently we only support relative paths at the application layer. We expect the codec to have - // broken the path into pieces if applicable. NOTE: Currently the HTTP/1.1 codec does not do this - // so we only support relative paths in all cases. https://tools.ietf.org/html/rfc7230#section-5.3 - // We also need to check for the existence of :path because CONNECT does not have a path, and we - // don't support that currently. + // broken the path into pieces if applicable. NOTE: Currently the HTTP/1.1 codec only does this + // when the allow_absolute_url flag is enabled on the HCM. + // https://tools.ietf.org/html/rfc7230#section-5.3 We also need to check for the existence of + // :path because CONNECT does not have a path, and we don't support that currently. if (!request_headers_->Path() || request_headers_->Path()->value().c_str()[0] != '/') { connection_manager_.stats_.named_.downstream_rq_non_relative_path_.inc(); sendLocalReply(Grpc::Common::hasGrpcContentType(*request_headers_), Code::NotFound, "", nullptr, diff --git a/source/common/ssl/utility.cc b/source/common/ssl/utility.cc index f4c4cf5824845..a1fe58dbbb1f1 100644 --- a/source/common/ssl/utility.cc +++ b/source/common/ssl/utility.cc @@ -59,6 +59,7 @@ std::string Utility::getSubjectFromCertificate(X509& cert) { } int32_t Utility::getDaysUntilExpiration(X509* cert) { + // TODO(lizan): Plumbing TimeSource to here. if (cert == nullptr) { return std::numeric_limits::max(); } diff --git a/source/extensions/extensions_build_config.bzl b/source/extensions/extensions_build_config.bzl index d18bfd70061a5..08e14ffe98e57 100644 --- a/source/extensions/extensions_build_config.bzl +++ b/source/extensions/extensions_build_config.bzl @@ -100,6 +100,7 @@ EXTENSIONS = { "envoy.tracers.dynamic_ot": "//source/extensions/tracers/dynamic_ot:config", "envoy.tracers.lightstep": "//source/extensions/tracers/lightstep:config", + "envoy.tracers.datadog": "//source/extensions/tracers/datadog:config", "envoy.tracers.zipkin": "//source/extensions/tracers/zipkin:config", # diff --git a/source/extensions/filters/common/rbac/matchers.cc b/source/extensions/filters/common/rbac/matchers.cc index 8a6030c21408a..97f859e716773 100644 --- a/source/extensions/filters/common/rbac/matchers.cc +++ b/source/extensions/filters/common/rbac/matchers.cc @@ -26,6 +26,8 @@ MatcherConstSharedPtr Matcher::create(const envoy::config::rbac::v2alpha::Permis return std::make_shared(permission.metadata()); case envoy::config::rbac::v2alpha::Permission::RuleCase::kNotRule: return std::make_shared(permission.not_rule()); + case envoy::config::rbac::v2alpha::Permission::RuleCase::kRequestedServerName: + return std::make_shared(permission.requested_server_name()); default: NOT_REACHED_GCOVR_EXCL_LINE; } @@ -157,6 +159,12 @@ bool PolicyMatcher::matches(const Network::Connection& connection, principals_.matches(connection, headers, metadata); } +bool RequestedServerNameMatcher::matches(const Network::Connection& connection, + const Envoy::Http::HeaderMap&, + const envoy::api::v2::core::Metadata&) const { + return match(connection.requestedServerName()); +} + } // namespace RBAC } // namespace Common } // namespace Filters diff --git a/source/extensions/filters/common/rbac/matchers.h b/source/extensions/filters/common/rbac/matchers.h index d6b2d27c901d9..34b06cce9bfb6 100644 --- a/source/extensions/filters/common/rbac/matchers.h +++ b/source/extensions/filters/common/rbac/matchers.h @@ -202,6 +202,19 @@ class MetadataMatcher : public Matcher { const Envoy::Matchers::MetadataMatcher matcher_; }; +/** + * Perform a match against the request server from the client's connection + * request. This is typically TLS SNI. + */ +class RequestedServerNameMatcher : public Matcher, Envoy::Matchers::StringMatcher { +public: + RequestedServerNameMatcher(const envoy::type::matcher::StringMatcher& requested_server_name) + : Envoy::Matchers::StringMatcher(requested_server_name) {} + + bool matches(const Network::Connection& connection, const Envoy::Http::HeaderMap& headers, + const envoy::api::v2::core::Metadata&) const override; +}; + } // namespace RBAC } // namespace Common } // namespace Filters diff --git a/source/extensions/tracers/datadog/BUILD b/source/extensions/tracers/datadog/BUILD new file mode 100644 index 0000000000000..e4ac3d95ba415 --- /dev/null +++ b/source/extensions/tracers/datadog/BUILD @@ -0,0 +1,37 @@ +licenses(["notice"]) # Apache 2 + +# Trace driver for Datadog (https://datadoghq.com/) + +load( + "//bazel:envoy_build_system.bzl", + "envoy_cc_library", + "envoy_package", +) + +envoy_package() + +envoy_cc_library( + name = "datadog_tracer_lib", + srcs = [ + "datadog_tracer_impl.cc", + ], + hdrs = [ + "datadog_tracer_impl.h", + ], + external_deps = ["dd_opentracing_cpp"], + deps = [ + "//source/common/tracing:http_tracer_lib", + "//source/extensions/tracers/common/ot:opentracing_driver_lib", + ], +) + +envoy_cc_library( + name = "config", + srcs = ["config.cc"], + hdrs = ["config.h"], + deps = [ + ":datadog_tracer_lib", + "//source/extensions/tracers:well_known_names", + "//source/server:configuration_lib", + ], +) diff --git a/source/extensions/tracers/datadog/config.cc b/source/extensions/tracers/datadog/config.cc new file mode 100644 index 0000000000000..2062f94d6958d --- /dev/null +++ b/source/extensions/tracers/datadog/config.cc @@ -0,0 +1,48 @@ +#include "extensions/tracers/datadog/config.h" + +#include "envoy/registry/registry.h" + +#include "common/common/utility.h" +#include "common/tracing/http_tracer_impl.h" + +#include "extensions/tracers/datadog/datadog_tracer_impl.h" +#include "extensions/tracers/well_known_names.h" + +#include "datadog/opentracing.h" + +namespace Envoy { +namespace Extensions { +namespace Tracers { +namespace Datadog { + +Tracing::HttpTracerPtr +DatadogTracerFactory::createHttpTracer(const envoy::config::trace::v2::Tracing& configuration, + Server::Instance& server) { + + ProtobufTypes::MessagePtr config_ptr = createEmptyConfigProto(); + + if (configuration.http().has_config()) { + MessageUtil::jsonConvert(configuration.http().config(), *config_ptr); + } + + const auto& datadog_config = + dynamic_cast(*config_ptr); + + Tracing::DriverPtr datadog_driver{new Driver{datadog_config, server.clusterManager(), + server.stats(), server.threadLocal(), + server.runtime()}}; + return std::make_unique(std::move(datadog_driver), server.localInfo()); +} + +std::string DatadogTracerFactory::name() { return TracerNames::get().Datadog; } + +/** + * Static registration for the Datadog tracer. @see RegisterFactory. + */ +static Registry::RegisterFactory + register_; + +} // namespace Datadog +} // namespace Tracers +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/tracers/datadog/config.h b/source/extensions/tracers/datadog/config.h new file mode 100644 index 0000000000000..a257faf8c6f94 --- /dev/null +++ b/source/extensions/tracers/datadog/config.h @@ -0,0 +1,33 @@ +#pragma once + +#include + +#include "envoy/server/instance.h" + +#include "server/configuration_impl.h" + +namespace Envoy { +namespace Extensions { +namespace Tracers { +namespace Datadog { + +/** + * Config registration for the Datadog tracer. @see TracerFactory. + */ +class DatadogTracerFactory : public Server::Configuration::TracerFactory { +public: + // TracerFactory + Tracing::HttpTracerPtr createHttpTracer(const envoy::config::trace::v2::Tracing& configuration, + Server::Instance& server) override; + + ProtobufTypes::MessagePtr createEmptyConfigProto() override { + return std::make_unique(); + } + + std::string name() override; +}; + +} // namespace Datadog +} // namespace Tracers +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/tracers/datadog/datadog_tracer_impl.cc b/source/extensions/tracers/datadog/datadog_tracer_impl.cc new file mode 100644 index 0000000000000..c68bcd3823d72 --- /dev/null +++ b/source/extensions/tracers/datadog/datadog_tracer_impl.cc @@ -0,0 +1,125 @@ +#include "extensions/tracers/datadog/datadog_tracer_impl.h" + +#include "common/common/enum_to_int.h" +#include "common/common/fmt.h" +#include "common/common/utility.h" +#include "common/http/headers.h" +#include "common/http/message_impl.h" +#include "common/http/utility.h" +#include "common/tracing/http_tracer_impl.h" + +namespace Envoy { +namespace Extensions { +namespace Tracers { +namespace Datadog { + +Driver::TlsTracer::TlsTracer(const std::shared_ptr& tracer, + TraceReporterPtr&& reporter, Driver& driver) + : tracer_(tracer), reporter_(std::move(reporter)), driver_(driver) {} + +Driver::Driver(const envoy::config::trace::v2::DatadogConfig& datadog_config, + Upstream::ClusterManager& cluster_manager, Stats::Store& stats, + ThreadLocal::SlotAllocator& tls, Runtime::Loader& runtime) + : OpenTracingDriver{stats}, + cm_(cluster_manager), tracer_stats_{DATADOG_TRACER_STATS( + POOL_COUNTER_PREFIX(stats, "tracing.datadog."))}, + tls_(tls.allocateSlot()), runtime_(runtime) { + + Upstream::ThreadLocalCluster* cluster = cm_.get(datadog_config.collector_cluster()); + if (!cluster) { + throw EnvoyException(fmt::format("{} collector cluster is not defined on cluster manager level", + datadog_config.collector_cluster())); + } + cluster_ = cluster->info(); + + tracer_options_.operation_name_override = "envoy.proxy"; + if (datadog_config.service_name().size() > 0) { + tracer_options_.service = datadog_config.service_name(); + } else { + tracer_options_.service = "envoy"; + } + tracer_options_.priority_sampling = datadog_config.priority_sampling(); + + tls_->set([this](Event::Dispatcher& dispatcher) -> ThreadLocal::ThreadLocalObjectSharedPtr { + auto tp = datadog::opentracing::makeTracerAndEncoder(tracer_options_); + auto tracer = std::get<0>(tp); + auto encoder = std::get<1>(tp); + TraceReporterPtr reporter(new TraceReporter(encoder, *this, dispatcher)); + return ThreadLocal::ThreadLocalObjectSharedPtr{ + new TlsTracer(tracer, std::move(reporter), *this)}; + }); +} + +opentracing::Tracer& Driver::tracer() { return *tls_->getTyped().tracer_; } + +TraceReporter::TraceReporter(TraceEncoderPtr encoder, Driver& driver, Event::Dispatcher& dispatcher) + : driver_(driver), encoder_(encoder) { + flush_timer_ = dispatcher.createTimer([this]() -> void { + driver_.tracerStats().timer_flushed_.inc(); + flushTraces(); + enableTimer(); + }); + + enableTimer(); +} + +void TraceReporter::enableTimer() { + const uint64_t flush_interval = + driver_.runtime().snapshot().getInteger("tracing.datadog.flush_interval_ms", 1000U); + flush_timer_->enableTimer(std::chrono::milliseconds(flush_interval)); +} + +void TraceReporter::flushTraces() { + auto pendingTraces = encoder_->pendingTraces(); + ENVOY_LOG(debug, "flushing traces: {} traces", pendingTraces); + if (pendingTraces) { + driver_.tracerStats().traces_sent_.add(pendingTraces); + + Http::MessagePtr message(new Http::RequestMessageImpl()); + message->headers().insertMethod().value().setReference(Http::Headers::get().MethodValues.Post); + message->headers().insertPath().value(encoder_->path()); + message->headers().insertHost().value(driver_.cluster()->name()); + for (auto& h : encoder_->headers()) { + ENVOY_LOG(debug, "Adding header {}: {}", h.first, h.second); + message->headers().addCopy(Http::LowerCaseString(h.first), h.second); + } + + Buffer::InstancePtr body(new Buffer::OwnedImpl()); + body->add(encoder_->payload()); + message->body() = std::move(body); + ENVOY_LOG(debug, "submitting {} trace(s) to {} with payload {}", pendingTraces, + encoder_->path(), encoder_->payload().size()); + + const uint64_t timeout = + driver_.runtime().snapshot().getInteger("tracing.datadog.request_timeout", 1000U); + driver_.clusterManager() + .httpAsyncClientForCluster(driver_.cluster()->name()) + .send(std::move(message), *this, std::chrono::milliseconds(timeout)); + + encoder_->clearTraces(); + } +} + +void TraceReporter::onFailure(Http::AsyncClient::FailureReason) { + ENVOY_LOG(debug, "failure submitting traces to datadog agent"); + driver_.tracerStats().reports_failed_.inc(); +} + +void TraceReporter::onSuccess(Http::MessagePtr&& http_response) { + uint64_t responseStatus = Http::Utility::getResponseStatus(http_response->headers()); + if (responseStatus != enumToInt(Http::Code::OK)) { + ENVOY_LOG(debug, "unexpected HTTP response code from datadog agent: {}", responseStatus); + driver_.tracerStats().reports_dropped_.inc(); + if (driver_.tracerOptions().priority_sampling) { + encoder_->handleResponse(http_response->body()->toString()); + } + } else { + ENVOY_LOG(debug, "traces successfully submitted to datadog agent"); + driver_.tracerStats().reports_sent_.inc(); + } +} + +} // namespace Datadog +} // namespace Tracers +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/tracers/datadog/datadog_tracer_impl.h b/source/extensions/tracers/datadog/datadog_tracer_impl.h new file mode 100644 index 0000000000000..20395f24e7e11 --- /dev/null +++ b/source/extensions/tracers/datadog/datadog_tracer_impl.h @@ -0,0 +1,131 @@ +#pragma once + +#include + +#include "envoy/local_info/local_info.h" +#include "envoy/runtime/runtime.h" +#include "envoy/thread_local/thread_local.h" +#include "envoy/tracing/http_tracer.h" +#include "envoy/upstream/cluster_manager.h" + +#include "common/http/header_map_impl.h" +#include "common/json/json_loader.h" + +#include "extensions/tracers/common/ot/opentracing_driver_impl.h" + +namespace Envoy { +namespace Extensions { +namespace Tracers { +namespace Datadog { + +#define DATADOG_TRACER_STATS(COUNTER) \ + COUNTER(traces_sent) \ + COUNTER(timer_flushed) \ + COUNTER(reports_sent) \ + COUNTER(reports_dropped) \ + COUNTER(reports_failed) + +struct DatadogTracerStats { + DATADOG_TRACER_STATS(GENERATE_COUNTER_STRUCT) +}; + +class TraceReporter; +typedef std::unique_ptr TraceReporterPtr; +typedef std::shared_ptr TraceEncoderPtr; + +/** + * Class for a Datadog-specific Driver. + */ +class Driver : public Common::Ot::OpenTracingDriver { +public: + /** + * Constructor. It adds itself and a newly-created Datadog::Tracer object to a thread-local store. + */ + Driver(const envoy::config::trace::v2::DatadogConfig& datadog_config, + Upstream::ClusterManager& cluster_manager, Stats::Store& stats, + ThreadLocal::SlotAllocator& tls, Runtime::Loader& runtime); + + // Tracer::OpenTracingDriver + opentracing::Tracer& tracer() override; + PropagationMode propagationMode() const override { + return Common::Ot::OpenTracingDriver::PropagationMode::TracerNative; + } + + // Getters to return the DatadogDriver's key members. + Upstream::ClusterManager& clusterManager() { return cm_; } + Upstream::ClusterInfoConstSharedPtr cluster() { return cluster_; } + Runtime::Loader& runtime() { return runtime_; } + DatadogTracerStats& tracerStats() { return tracer_stats_; } + const datadog::opentracing::TracerOptions& tracerOptions() { return tracer_options_; } + +private: + /** + * Thread-local store containing DatadogDriver and Datadog::Tracer objects. + */ + struct TlsTracer : ThreadLocal::ThreadLocalObject { + TlsTracer(const std::shared_ptr& tracer, TraceReporterPtr&& reporter, + Driver& driver); + + std::shared_ptr tracer_; + TraceReporterPtr reporter_; + Driver& driver_; + }; + + Upstream::ClusterManager& cm_; + Upstream::ClusterInfoConstSharedPtr cluster_; + DatadogTracerStats tracer_stats_; + datadog::opentracing::TracerOptions tracer_options_; + ThreadLocal::SlotPtr tls_; + Runtime::Loader& runtime_; +}; + +/** + * This class wraps the encoder provided with the tracer at initialization + * and uses Http::AsyncClient to send completed traces to the Datadog Agent. + * + * The cluster to use for submitting traces to the agent is controlled with + * the setting tracing.datadog.collector_cluster, which is mandatory and must + * refer to a cluster in the active configuration. + * + * An internal timer is used to control how often traces are submitted. + * If zero traces have completed in the interval between timer events, + * no action is taken. + * The timer interval can be controlled with the setting + * tracing.datadog.flush_interval_ms, and defaults to 2000ms. + */ +class TraceReporter : public Http::AsyncClient::Callbacks, + protected Logger::Loggable { +public: + /** + * Constructor. + * + * @param encoder Provides methods to retrieve data for publishing traces. + * @param driver The driver to be associated with the reporter. + * @param dispatcher Controls the timer used to flush buffered traces. + */ + TraceReporter(TraceEncoderPtr encoder, Driver& driver, Event::Dispatcher& dispatcher); + + // Http::AsyncClient::Callbacks. + void onSuccess(Http::MessagePtr&&) override; + void onFailure(Http::AsyncClient::FailureReason) override; + +private: + /** + * Enables the trace-flushing timer. + */ + void enableTimer(); + + /** + * Removes all traces from the trace buffer and sends them to a Datadog Agent using + * Http::AsyncClient. + */ + void flushTraces(); + + Driver& driver_; + Event::TimerPtr flush_timer_; + TraceEncoderPtr encoder_; +}; +} // namespace Datadog +} // namespace Tracers +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/tracers/well_known_names.h b/source/extensions/tracers/well_known_names.h index d545eecedad97..43f1181d572d9 100644 --- a/source/extensions/tracers/well_known_names.h +++ b/source/extensions/tracers/well_known_names.h @@ -18,6 +18,8 @@ class TracerNameValues { const std::string Zipkin = "envoy.zipkin"; // Dynamic tracer const std::string DynamicOt = "envoy.dynamic.ot"; + // Datadog tracer + const std::string Datadog = "envoy.tracers.datadog"; }; typedef ConstSingleton TracerNames; diff --git a/test/common/ssl/utility_test.cc b/test/common/ssl/utility_test.cc index ac8b21e195674..0eea1cebac303 100644 --- a/test/common/ssl/utility_test.cc +++ b/test/common/ssl/utility_test.cc @@ -52,7 +52,7 @@ TEST(UtilityTest, TestGetSerialNumber) { TEST(UtilityTest, TestDaysUntilExpiration) { bssl::UniquePtr cert = readCertFromFile("test/common/ssl/test_data/san_dns_cert.pem"); - EXPECT_EQ(270, Utility::getDaysUntilExpiration(cert.get())); + EXPECT_LE(0, Utility::getDaysUntilExpiration(cert.get())); } TEST(UtilityTest, TestDaysUntilExpirationWithNull) { @@ -60,4 +60,4 @@ TEST(UtilityTest, TestDaysUntilExpirationWithNull) { } } // namespace Ssl -} // namespace Envoy \ No newline at end of file +} // namespace Envoy diff --git a/test/extensions/filters/common/rbac/matchers_test.cc b/test/extensions/filters/common/rbac/matchers_test.cc index caee42c763715..4e1c07e43eb54 100644 --- a/test/extensions/filters/common/rbac/matchers_test.cc +++ b/test/extensions/filters/common/rbac/matchers_test.cc @@ -290,6 +290,46 @@ TEST(PolicyMatcher, PolicyMatcher) { checkMatcher(matcher, false, conn); } +const envoy::type::matcher::StringMatcher createRegexMatcher(std::string str) { + envoy::type::matcher::StringMatcher matcher; + matcher.set_regex(str); + return matcher; +} + +const envoy::type::matcher::StringMatcher createExactMatcher(std::string str) { + envoy::type::matcher::StringMatcher matcher; + matcher.set_exact(str); + return matcher; +} + +TEST(RequestedServerNameMatcher, ValidRequestedServerName) { + Envoy::Network::MockConnection conn; + EXPECT_CALL(conn, requestedServerName()) + .Times(9) + .WillRepeatedly(Return(absl::string_view("www.cncf.io"))); + + checkMatcher(RequestedServerNameMatcher(createRegexMatcher(".*cncf.io")), true, conn); + checkMatcher(RequestedServerNameMatcher(createRegexMatcher(".*cncf.*")), true, conn); + checkMatcher(RequestedServerNameMatcher(createRegexMatcher("www.*")), true, conn); + checkMatcher(RequestedServerNameMatcher(createRegexMatcher(".*io")), true, conn); + checkMatcher(RequestedServerNameMatcher(createRegexMatcher(".*")), true, conn); + + checkMatcher(RequestedServerNameMatcher(createExactMatcher("")), false, conn); + checkMatcher(RequestedServerNameMatcher(createExactMatcher("www.cncf.io")), true, conn); + checkMatcher(RequestedServerNameMatcher(createExactMatcher("xyz.cncf.io")), false, conn); + checkMatcher(RequestedServerNameMatcher(createExactMatcher("example.com")), false, conn); +} + +TEST(RequestedServerNameMatcher, EmptyRequestedServerName) { + Envoy::Network::MockConnection conn; + EXPECT_CALL(conn, requestedServerName()).Times(3).WillRepeatedly(Return(absl::string_view(""))); + + checkMatcher(RequestedServerNameMatcher(createRegexMatcher(".*")), true, conn); + + checkMatcher(RequestedServerNameMatcher(createExactMatcher("")), true, conn); + checkMatcher(RequestedServerNameMatcher(createExactMatcher("example.com")), false, conn); +} + } // namespace } // namespace RBAC } // namespace Common diff --git a/test/extensions/filters/http/rbac/rbac_filter_test.cc b/test/extensions/filters/http/rbac/rbac_filter_test.cc index ce6b37184750a..ab585f7f0f3cc 100644 --- a/test/extensions/filters/http/rbac/rbac_filter_test.cc +++ b/test/extensions/filters/http/rbac/rbac_filter_test.cc @@ -27,13 +27,17 @@ class RoleBasedAccessControlFilterTest : public testing::Test { envoy::config::filter::http::rbac::v2::RBAC config; envoy::config::rbac::v2alpha::Policy policy; - policy.add_permissions()->set_destination_port(123); + auto policy_rules = policy.add_permissions()->mutable_or_rules(); + policy_rules->add_rules()->mutable_requested_server_name()->set_regex(".*cncf.io"); + policy_rules->add_rules()->set_destination_port(123); policy.add_principals()->set_any(true); config.mutable_rules()->set_action(envoy::config::rbac::v2alpha::RBAC::ALLOW); (*config.mutable_rules()->mutable_policies())["foo"] = policy; envoy::config::rbac::v2alpha::Policy shadow_policy; - shadow_policy.add_permissions()->set_destination_port(456); + auto shadow_policy_rules = shadow_policy.add_permissions()->mutable_or_rules(); + shadow_policy_rules->add_rules()->mutable_requested_server_name()->set_exact("xyz.cncf.io"); + shadow_policy_rules->add_rules()->set_destination_port(456); shadow_policy.add_principals()->set_any(true); config.mutable_shadow_rules()->set_action(envoy::config::rbac::v2alpha::RBAC::ALLOW); (*config.mutable_shadow_rules()->mutable_policies())["bar"] = shadow_policy; @@ -54,6 +58,11 @@ class RoleBasedAccessControlFilterTest : public testing::Test { ON_CALL(connection_, localAddress()).WillByDefault(ReturnRef(address_)); } + void setRequestedServerName(std::string server_name) { + requested_server_name_ = server_name; + ON_CALL(connection_, requestedServerName()).WillByDefault(Return(requested_server_name_)); + } + void setMetadata() { ON_CALL(req_info_, setDynamicMetadata(HttpFilterNames::get().Rbac, _)) .WillByDefault(Invoke([this](const std::string&, const ProtobufWkt::Struct& obj) { @@ -71,6 +80,7 @@ class RoleBasedAccessControlFilterTest : public testing::Test { RoleBasedAccessControlFilter filter_; Network::Address::InstanceConstSharedPtr address_; + std::string requested_server_name_; Http::TestHeaderMapImpl headers_; }; @@ -86,6 +96,21 @@ TEST_F(RoleBasedAccessControlFilterTest, Allowed) { EXPECT_EQ(Http::FilterTrailersStatus::Continue, filter_.decodeTrailers(headers_)); } +TEST_F(RoleBasedAccessControlFilterTest, RequestedServerName) { + setDestinationPort(999); + setRequestedServerName("www.cncf.io"); + + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_.decodeHeaders(headers_, false)); + EXPECT_EQ(1U, config_->stats().allowed_.value()); + EXPECT_EQ(0U, config_->stats().denied_.value()); + EXPECT_EQ(0U, config_->stats().shadow_allowed_.value()); + EXPECT_EQ(1U, config_->stats().shadow_denied_.value()); + + Buffer::OwnedImpl data(""); + EXPECT_EQ(Http::FilterDataStatus::Continue, filter_.decodeData(data, false)); + EXPECT_EQ(Http::FilterTrailersStatus::Continue, filter_.decodeTrailers(headers_)); +} + TEST_F(RoleBasedAccessControlFilterTest, Denied) { setDestinationPort(456); setMetadata(); diff --git a/test/extensions/filters/network/rbac/filter_test.cc b/test/extensions/filters/network/rbac/filter_test.cc index 0433b711d3a3c..c71117ca73cd9 100644 --- a/test/extensions/filters/network/rbac/filter_test.cc +++ b/test/extensions/filters/network/rbac/filter_test.cc @@ -6,6 +6,7 @@ #include "test/mocks/network/mocks.h" using testing::NiceMock; +using testing::Return; using testing::ReturnRef; namespace Envoy { @@ -21,13 +22,17 @@ class RoleBasedAccessControlNetworkFilterTest : public testing::Test { if (with_policy) { envoy::config::rbac::v2alpha::Policy policy; - policy.add_permissions()->set_destination_port(123); + auto policy_rules = policy.add_permissions()->mutable_or_rules(); + policy_rules->add_rules()->mutable_requested_server_name()->set_regex(".*cncf.io"); + policy_rules->add_rules()->set_destination_port(123); policy.add_principals()->set_any(true); config.mutable_rules()->set_action(envoy::config::rbac::v2alpha::RBAC::ALLOW); (*config.mutable_rules()->mutable_policies())["foo"] = policy; envoy::config::rbac::v2alpha::Policy shadow_policy; - shadow_policy.add_permissions()->set_destination_port(456); + auto shadow_policy_rules = shadow_policy.add_permissions()->mutable_or_rules(); + shadow_policy_rules->add_rules()->mutable_requested_server_name()->set_exact("xyz.cncf.io"); + shadow_policy_rules->add_rules()->set_destination_port(456); shadow_policy.add_principals()->set_any(true); config.mutable_shadow_rules()->set_action(envoy::config::rbac::v2alpha::RBAC::ALLOW); (*config.mutable_shadow_rules()->mutable_policies())["bar"] = shadow_policy; @@ -46,6 +51,12 @@ class RoleBasedAccessControlNetworkFilterTest : public testing::Test { EXPECT_CALL(callbacks_.connection_, localAddress()).WillRepeatedly(ReturnRef(address_)); } + void setRequestedServerName(std::string server_name) { + requested_server_name_ = server_name; + ON_CALL(callbacks_.connection_, requestedServerName()) + .WillByDefault(Return(requested_server_name_)); + } + NiceMock callbacks_; Stats::IsolatedStoreImpl store_; Buffer::OwnedImpl data_; @@ -53,6 +64,7 @@ class RoleBasedAccessControlNetworkFilterTest : public testing::Test { std::unique_ptr filter_; Network::Address::InstanceConstSharedPtr address_; + std::string requested_server_name_; }; TEST_F(RoleBasedAccessControlNetworkFilterTest, Allowed) { @@ -69,6 +81,21 @@ TEST_F(RoleBasedAccessControlNetworkFilterTest, Allowed) { EXPECT_EQ(1U, config_->stats().shadow_denied_.value()); } +TEST_F(RoleBasedAccessControlNetworkFilterTest, RequestedServerName) { + setDestinationPort(999); + setRequestedServerName("www.cncf.io"); + + EXPECT_EQ(Network::FilterStatus::Continue, filter_->onNewConnection()); + + // Call onData() twice, should only increase stats once. + EXPECT_EQ(Network::FilterStatus::Continue, filter_->onData(data_, false)); + EXPECT_EQ(Network::FilterStatus::Continue, filter_->onData(data_, false)); + EXPECT_EQ(1U, config_->stats().allowed_.value()); + EXPECT_EQ(0U, config_->stats().denied_.value()); + EXPECT_EQ(0U, config_->stats().shadow_allowed_.value()); + EXPECT_EQ(1U, config_->stats().shadow_denied_.value()); +} + TEST_F(RoleBasedAccessControlNetworkFilterTest, AllowedWithNoPolicy) { config_ = setupConfig(false /* with_policy */); filter_.reset(new RoleBasedAccessControlFilter(config_)); diff --git a/test/extensions/tracers/datadog/BUILD b/test/extensions/tracers/datadog/BUILD new file mode 100644 index 0000000000000..714c3091bad41 --- /dev/null +++ b/test/extensions/tracers/datadog/BUILD @@ -0,0 +1,48 @@ +licenses(["notice"]) # Apache 2 + +load( + "//bazel:envoy_build_system.bzl", + "envoy_package", +) +load( + "//test/extensions:extensions_build_system.bzl", + "envoy_extension_cc_test", +) + +envoy_package() + +envoy_extension_cc_test( + name = "datadog_tracer_impl_test", + srcs = [ + "datadog_tracer_impl_test.cc", + ], + extension_name = "envoy.tracers.datadog", + deps = [ + "//source/common/common:base64_lib", + "//source/common/http:header_map_lib", + "//source/common/http:headers_lib", + "//source/common/http:message_lib", + "//source/common/runtime:runtime_lib", + "//source/common/runtime:uuid_util_lib", + "//source/extensions/tracers/datadog:datadog_tracer_lib", + "//test/mocks/http:http_mocks", + "//test/mocks/local_info:local_info_mocks", + "//test/mocks/runtime:runtime_mocks", + "//test/mocks/stats:stats_mocks", + "//test/mocks/thread_local:thread_local_mocks", + "//test/mocks/tracing:tracing_mocks", + "//test/mocks/upstream:upstream_mocks", + "//test/test_common:utility_lib", + ], +) + +envoy_extension_cc_test( + name = "config_test", + srcs = ["config_test.cc"], + extension_name = "envoy.tracers.datadog", + deps = [ + "//source/extensions/tracers/datadog:config", + "//test/mocks/server:server_mocks", + "//test/test_common:utility_lib", + ], +) diff --git a/test/extensions/tracers/datadog/config_test.cc b/test/extensions/tracers/datadog/config_test.cc new file mode 100644 index 0000000000000..7a5bec96dac5f --- /dev/null +++ b/test/extensions/tracers/datadog/config_test.cc @@ -0,0 +1,43 @@ +#include "extensions/tracers/datadog/config.h" + +#include "test/mocks/server/mocks.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +using testing::_; +using testing::NiceMock; +using testing::Return; + +namespace Envoy { +namespace Extensions { +namespace Tracers { +namespace Datadog { + +TEST(DatadogTracerConfigTest, DatadogHttpTracer) { + NiceMock server; + EXPECT_CALL(server.cluster_manager_, get("fake_cluster")) + .WillRepeatedly(Return(&server.cluster_manager_.thread_local_cluster_)); + ON_CALL(*server.cluster_manager_.thread_local_cluster_.cluster_.info_, features()) + .WillByDefault(Return(Upstream::ClusterInfo::Features::HTTP2)); + + const std::string yaml_string = R"EOF( + http: + name: envoy.tracers.datadog + config: + collector_cluster: fake_cluster + service_name: fake_file + priority_sampling: true + )EOF"; + envoy::config::trace::v2::Tracing configuration; + MessageUtil::loadFromYaml(yaml_string, configuration); + + DatadogTracerFactory factory; + Tracing::HttpTracerPtr datadog_tracer = factory.createHttpTracer(configuration, server); + EXPECT_NE(nullptr, datadog_tracer); +} + +} // namespace Datadog +} // namespace Tracers +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/tracers/datadog/datadog_tracer_impl_test.cc b/test/extensions/tracers/datadog/datadog_tracer_impl_test.cc new file mode 100644 index 0000000000000..0f5b1b75312fb --- /dev/null +++ b/test/extensions/tracers/datadog/datadog_tracer_impl_test.cc @@ -0,0 +1,169 @@ +#include +#include +#include +#include + +#include "common/common/base64.h" +#include "common/http/header_map_impl.h" +#include "common/http/headers.h" +#include "common/http/message_impl.h" +#include "common/runtime/runtime_impl.h" +#include "common/runtime/uuid_util.h" +#include "common/tracing/http_tracer_impl.h" + +#include "extensions/tracers/datadog/datadog_tracer_impl.h" + +#include "test/mocks/http/mocks.h" +#include "test/mocks/local_info/mocks.h" +#include "test/mocks/runtime/mocks.h" +#include "test/mocks/stats/mocks.h" +#include "test/mocks/thread_local/mocks.h" +#include "test/mocks/tracing/mocks.h" +#include "test/mocks/upstream/mocks.h" +#include "test/test_common/printers.h" +#include "test/test_common/utility.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +using testing::_; +using testing::AtLeast; +using testing::Invoke; +using testing::NiceMock; +using testing::Return; +using testing::ReturnRef; +using testing::Test; + +namespace Envoy { +namespace Extensions { +namespace Tracers { +namespace Datadog { + +class DatadogDriverTest : public Test { +public: + void setup(envoy::config::trace::v2::DatadogConfig& datadog_config, bool init_timer) { + ON_CALL(cm_, httpAsyncClientForCluster("fake_cluster")) + .WillByDefault(ReturnRef(cm_.async_client_)); + + if (init_timer) { + timer_ = new NiceMock(&tls_.dispatcher_); + EXPECT_CALL(*timer_, enableTimer(std::chrono::milliseconds(1000))); + } + + driver_.reset(new Driver{datadog_config, cm_, stats_, tls_, runtime_}); + } + + void setupValidDriver() { + EXPECT_CALL(cm_, get("fake_cluster")).WillRepeatedly(Return(&cm_.thread_local_cluster_)); + ON_CALL(*cm_.thread_local_cluster_.cluster_.info_, features()) + .WillByDefault(Return(Upstream::ClusterInfo::Features::HTTP2)); + + const std::string yaml_string = R"EOF( + collector_cluster: fake_cluster + )EOF"; + envoy::config::trace::v2::DatadogConfig datadog_config; + MessageUtil::loadFromYaml(yaml_string, datadog_config); + + setup(datadog_config, true); + } + + const std::string operation_name_{"test"}; + Http::TestHeaderMapImpl request_headers_{ + {":path", "/"}, {":method", "GET"}, {"x-request-id", "foo"}}; + const Http::TestHeaderMapImpl response_headers_{{":status", "500"}}; + SystemTime start_time_; + + NiceMock tls_; + std::unique_ptr driver_; + NiceMock* timer_; + Stats::IsolatedStoreImpl stats_; + NiceMock cm_; + NiceMock random_; + NiceMock runtime_; + NiceMock local_info_; + + NiceMock config_; +}; + +TEST_F(DatadogDriverTest, InitializeDriver) { + { + envoy::config::trace::v2::DatadogConfig datadog_config; + + EXPECT_THROW(setup(datadog_config, false), EnvoyException); + } + + { + // Valid config but not valid cluster. + EXPECT_CALL(cm_, get("fake_cluster")).WillOnce(Return(nullptr)); + + const std::string yaml_string = R"EOF( + collector_cluster: fake_cluster + )EOF"; + envoy::config::trace::v2::DatadogConfig datadog_config; + MessageUtil::loadFromYaml(yaml_string, datadog_config); + + EXPECT_THROW(setup(datadog_config, false), EnvoyException); + } + + { + EXPECT_CALL(cm_, get("fake_cluster")).WillRepeatedly(Return(&cm_.thread_local_cluster_)); + ON_CALL(*cm_.thread_local_cluster_.cluster_.info_, features()) + .WillByDefault(Return(Upstream::ClusterInfo::Features::HTTP2)); + + const std::string yaml_string = R"EOF( + collector_cluster: fake_cluster + )EOF"; + envoy::config::trace::v2::DatadogConfig datadog_config; + MessageUtil::loadFromYaml(yaml_string, datadog_config); + + setup(datadog_config, true); + } +} + +TEST_F(DatadogDriverTest, FlushSpansTimer) { + setupValidDriver(); + + Http::MockAsyncClientRequest request(&cm_.async_client_); + Http::AsyncClient::Callbacks* callback; + const absl::optional timeout(std::chrono::seconds(1)); + EXPECT_CALL(cm_.async_client_, send_(_, _, timeout)) + .WillOnce(Invoke( + [&](Http::MessagePtr& message, Http::AsyncClient::Callbacks& callbacks, + const absl::optional&) -> Http::AsyncClient::Request* { + callback = &callbacks; + + EXPECT_STREQ("fake_cluster", message->headers().Host()->value().c_str()); + EXPECT_STREQ("application/msgpack", message->headers().ContentType()->value().c_str()); + + return &request; + })); + + Tracing::SpanPtr span = driver_->startSpan(config_, request_headers_, operation_name_, + start_time_, {Tracing::Reason::Sampling, true}); + span->finishSpan(); + + // Timer should be re-enabled. + EXPECT_CALL(*timer_, enableTimer(std::chrono::milliseconds(1000))); + EXPECT_CALL(runtime_.snapshot_, getInteger("tracing.datadog.request_timeout", 1000U)) + .WillOnce(Return(1000U)); + EXPECT_CALL(runtime_.snapshot_, getInteger("tracing.datadog.flush_interval_ms", 1000U)) + .WillOnce(Return(1000U)); + + timer_->callback_(); + + EXPECT_EQ(1U, stats_.counter("tracing.datadog.timer_flushed").value()); + EXPECT_EQ(1U, stats_.counter("tracing.datadog.traces_sent").value()); + + Http::MessagePtr msg(new Http::ResponseMessageImpl( + Http::HeaderMapPtr{new Http::TestHeaderMapImpl{{":status", "200"}}})); + callback->onSuccess(std::move(msg)); + + EXPECT_EQ(1U, stats_.counter("tracing.datadog.reports_sent").value()); + EXPECT_EQ(0U, stats_.counter("tracing.datadog.reports_dropped").value()); + EXPECT_EQ(0U, stats_.counter("tracing.datadog.reports_failed").value()); +} + +} // namespace Datadog +} // namespace Tracers +} // namespace Extensions +} // namespace Envoy