diff --git a/api/contrib/envoy/extensions/filters/network/smtp_proxy/v3alpha/BUILD b/api/contrib/envoy/extensions/filters/network/smtp_proxy/v3alpha/BUILD new file mode 100644 index 000000000000..0b02b988e42f --- /dev/null +++ b/api/contrib/envoy/extensions/filters/network/smtp_proxy/v3alpha/BUILD @@ -0,0 +1,12 @@ +# DO NOT EDIT. This file is generated by tools/proto_format/proto_sync.py. + +load("@envoy_api//bazel:api_build_system.bzl", "api_proto_package") + +licenses(["notice"]) # Apache 2 + +api_proto_package( + deps = [ + "//envoy/config/accesslog/v3:pkg", + "@com_github_cncf_udpa//udpa/annotations:pkg", + ], +) diff --git a/api/contrib/envoy/extensions/filters/network/smtp_proxy/v3alpha/smtp_proxy.proto b/api/contrib/envoy/extensions/filters/network/smtp_proxy/v3alpha/smtp_proxy.proto new file mode 100644 index 000000000000..eb05987a7ab0 --- /dev/null +++ b/api/contrib/envoy/extensions/filters/network/smtp_proxy/v3alpha/smtp_proxy.proto @@ -0,0 +1,47 @@ +syntax = "proto3"; + +package envoy.extensions.filters.network.smtp_proxy.v3alpha; + +import "envoy/config/accesslog/v3/accesslog.proto"; + +import "udpa/annotations/status.proto"; +import "validate/validate.proto"; + +option java_package = "io.envoyproxy.envoy.extensions.filters.network.smtp_proxy.v3alpha"; +option java_outer_classname = "SmtpProxyProto"; +option java_multiple_files = true; +option go_package = "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/smtp_proxy/v3alpha"; +option (udpa.annotations.file_status).work_in_progress = true; +option (udpa.annotations.file_status).package_version_status = ACTIVE; + +// [#protodoc-title: SMTP Proxy] +// SMTP Proxy :ref:`configuration overview +// `. +// [#extension: envoy.filters.network.smtp_proxy] + +message SmtpProxy { + // Upstream TLS operational modes. + enum UpstreamTLSMode { + // Do not encrypt upstream connection to the server. + DISABLE = 0; + + // Establish upstream TLS connection to the server. If the server does not + // accept the request for TLS connection, the session is terminated. + REQUIRE = 1; + } + + // The human readable prefix to use when emitting :ref:`statistics + // `. + string stat_prefix = 1 [(validate.rules).string = {min_len: 1}]; + + // If enabled, filter will generate x-req-id to identify smtp session/transaction and send it to upstream. + bool tracing = 2; + + // Controls whether to establish upstream TLS connection to the server. + // Defaults to DISABLE. + UpstreamTLSMode upstream_tls = 3; + + // Configuration for :ref:`access logs ` + // emitted by the SMTP Filter. + repeated config.accesslog.v3.AccessLog access_log = 4; +} diff --git a/api/envoy/config/filter/http/jwt_authn/v2alpha/config.proto b/api/envoy/config/filter/http/jwt_authn/v2alpha/config.proto index e87c9478db63..00ce562f8dfb 100644 --- a/api/envoy/config/filter/http/jwt_authn/v2alpha/config.proto +++ b/api/envoy/config/filter/http/jwt_authn/v2alpha/config.proto @@ -391,8 +391,7 @@ message FilterStateRule { // A map of string keys to requirements. The string key is the string value // in the FilterState with the name specified in the *name* field above. - map - requires = 3; + map requires = 3; } // This is the Envoy HTTP filter config for JWT authentication. diff --git a/api/envoy/extensions/filters/http/jwt_authn/v3/config.proto b/api/envoy/extensions/filters/http/jwt_authn/v3/config.proto index 11e6af453af0..850754707214 100644 --- a/api/envoy/extensions/filters/http/jwt_authn/v3/config.proto +++ b/api/envoy/extensions/filters/http/jwt_authn/v3/config.proto @@ -611,8 +611,7 @@ message FilterStateRule { // A map of string keys to requirements. The string key is the string value // in the FilterState with the name specified in the ``name`` field above. - map - requires = 3; + map requires = 3; } // This is the Envoy HTTP filter config for JWT authentication. diff --git a/api/versioning/BUILD b/api/versioning/BUILD index 309506ce5dfe..3b4f247cf543 100644 --- a/api/versioning/BUILD +++ b/api/versioning/BUILD @@ -29,6 +29,7 @@ proto_library( "//contrib/envoy/extensions/filters/network/sip_proxy/router/v3alpha:pkg", "//contrib/envoy/extensions/filters/network/sip_proxy/tra/v3alpha:pkg", "//contrib/envoy/extensions/filters/network/sip_proxy/v3alpha:pkg", + "//contrib/envoy/extensions/filters/network/smtp_proxy/v3alpha:pkg", "//contrib/envoy/extensions/matching/input_matchers/hyperscan/v3alpha:pkg", "//contrib/envoy/extensions/network/connection_balance/dlb/v3alpha:pkg", "//contrib/envoy/extensions/private_key_providers/cryptomb/v3alpha:pkg", diff --git a/contrib/contrib_build_config.bzl b/contrib/contrib_build_config.bzl index f2724aeb7df3..45bee45268eb 100644 --- a/contrib/contrib_build_config.bzl +++ b/contrib/contrib_build_config.bzl @@ -4,71 +4,71 @@ CONTRIB_EXTENSIONS = { # HTTP filters # - "envoy.filters.http.dynamo": "//contrib/dynamo/filters/http/source:config", - "envoy.filters.http.golang": "//contrib/golang/filters/http/source:config", - "envoy.filters.http.language": "//contrib/language/filters/http/source:config_lib", - "envoy.filters.http.squash": "//contrib/squash/filters/http/source:config", - "envoy.filters.http.sxg": "//contrib/sxg/filters/http/source:config", + # "envoy.filters.http.dynamo": "//contrib/dynamo/filters/http/source:config", + # "envoy.filters.http.golang": "//contrib/golang/filters/http/source:config", + # "envoy.filters.http.language": "//contrib/language/filters/http/source:config_lib", + # "envoy.filters.http.squash": "//contrib/squash/filters/http/source:config", + # "envoy.filters.http.sxg": "//contrib/sxg/filters/http/source:config", # # Network filters # - "envoy.filters.network.client_ssl_auth": "//contrib/client_ssl_auth/filters/network/source:config", - "envoy.filters.network.kafka_broker": "//contrib/kafka/filters/network/source:kafka_broker_config_lib", - "envoy.filters.network.kafka_mesh": "//contrib/kafka/filters/network/source/mesh:config_lib", - "envoy.filters.network.mysql_proxy": "//contrib/mysql_proxy/filters/network/source:config", - "envoy.filters.network.postgres_proxy": "//contrib/postgres_proxy/filters/network/source:config", - "envoy.filters.network.rocketmq_proxy": "//contrib/rocketmq_proxy/filters/network/source:config", + # "envoy.filters.network.client_ssl_auth": "//contrib/client_ssl_auth/filters/network/source:config", + # "envoy.filters.network.kafka_broker": "//contrib/kafka/filters/network/source:kafka_broker_config_lib", + # "envoy.filters.network.kafka_mesh": "//contrib/kafka/filters/network/source/mesh:config_lib", + # "envoy.filters.network.mysql_proxy": "//contrib/mysql_proxy/filters/network/source:config", + # "envoy.filters.network.postgres_proxy": "//contrib/postgres_proxy/filters/network/source:config", + # "envoy.filters.network.rocketmq_proxy": "//contrib/rocketmq_proxy/filters/network/source:config", "envoy.filters.network.generic_proxy": "//contrib/generic_proxy/filters/network/source:config", - + "envoy.filters.network.smtp_proxy": "//contrib/smtp_proxy/filters/network/source:config", # # Sip proxy # - "envoy.filters.network.sip_proxy": "//contrib/sip_proxy/filters/network/source:config", - "envoy.filters.sip.router": "//contrib/sip_proxy/filters/network/source/router:config", + # "envoy.filters.network.sip_proxy": "//contrib/sip_proxy/filters/network/source:config", + # "envoy.filters.sip.router": "//contrib/sip_proxy/filters/network/source/router:config", # # Private key providers # - "envoy.tls.key_providers.cryptomb": "//contrib/cryptomb/private_key_providers/source:config", - "envoy.tls.key_providers.qat": "//contrib/qat/private_key_providers/source:config", + # "envoy.tls.key_providers.cryptomb": "//contrib/cryptomb/private_key_providers/source:config", + # "envoy.tls.key_providers.qat": "//contrib/qat/private_key_providers/source:config", # # Socket interface extensions # - "envoy.bootstrap.vcl": "//contrib/vcl/source:config", + # "envoy.bootstrap.vcl": "//contrib/vcl/source:config", # # Input matchers # - "envoy.matching.input_matchers.hyperscan": "//contrib/hyperscan/matching/input_matchers/source:config", + # "envoy.matching.input_matchers.hyperscan": "//contrib/hyperscan/matching/input_matchers/source:config", # # Connection Balance extensions # - "envoy.network.connection_balance.dlb": "//contrib/network/connection_balance/dlb/source:connection_balancer", + # "envoy.network.connection_balance.dlb": "//contrib/network/connection_balance/dlb/source:connection_balancer", # # Regex engines # - "envoy.regex_engines.hyperscan": "//contrib/hyperscan/regex_engines/source:config", + # "envoy.regex_engines.hyperscan": "//contrib/hyperscan/regex_engines/source:config", # # Extensions for generic proxy # - "envoy.filters.generic.router": "//contrib/generic_proxy/filters/network/source/router:config", - "envoy.generic_proxy.codecs.dubbo": "//contrib/generic_proxy/filters/network/source/codecs/dubbo:config", + # "envoy.filters.generic.router": "//contrib/generic_proxy/filters/network/source/router:config", + # "envoy.generic_proxy.codecs.dubbo": "//contrib/generic_proxy/filters/network/source/codecs/dubbo:config", # # xDS delegates # - "envoy.xds_delegates.kv_store": "//contrib/config/source:kv_store_xds_delegate", + # "envoy.xds_delegates.kv_store": "//contrib/config/source:kv_store_xds_delegate", } diff --git a/contrib/extensions_metadata.yaml b/contrib/extensions_metadata.yaml index 9300e8e4d471..a96192bd77e2 100644 --- a/contrib/extensions_metadata.yaml +++ b/contrib/extensions_metadata.yaml @@ -117,3 +117,8 @@ envoy.generic_proxy.codecs.dubbo: status: wip type_urls: - envoy.extensions.filters.network.generic_proxy.codecs.dubbo.v3.DubboCodecConfig +envoy.filters.network.smtp_proxy: + categories: + - envoy.filters.network + security_posture: requires_trusted_downstream_and_upstream + status: alpha \ No newline at end of file diff --git a/contrib/smtp_proxy/filters/network/source/BUILD b/contrib/smtp_proxy/filters/network/source/BUILD new file mode 100644 index 000000000000..b2ba8464cb16 --- /dev/null +++ b/contrib/smtp_proxy/filters/network/source/BUILD @@ -0,0 +1,109 @@ +load( + "//bazel:envoy_build_system.bzl", + "envoy_cc_contrib_extension", + "envoy_cc_library", + "envoy_contrib_package", +) + +licenses(["notice"]) # Apache 2 + +envoy_contrib_package() + +#package(default_visibility = ["//visibility:public"]) + +# SMTP proxy L7 network filter. +# Public docs: https://envoyproxy.io/docs/envoy/latest/configuration/listeners/network_filters/smtp_proxy_filter + +envoy_cc_library( + name = "filter", + srcs = [ + "smtp_filter.cc", + ], + hdrs = [ + "smtp_filter.h", + ], + repository = "@envoy", + deps = [ + "smtp_decoder_lib", + "//envoy/access_log:access_log_interface", + "//envoy/network:filter_interface", + "//envoy/server:filter_config_interface", + "//envoy/stats:stats_interface", + "//envoy/stats:stats_macros", + "//source/common/buffer:buffer_lib", + "//source/common/network:filter_lib", + "//source/extensions/filters/network:well_known_names", + "@envoy_api//contrib/envoy/extensions/filters/network/smtp_proxy/v3alpha:pkg_cc_proto", + ], +) + +envoy_cc_library( + name = "smtp_decoder_lib", + srcs = ["smtp_decoder_impl.cc"], + hdrs = [ + "smtp_decoder.h", + "smtp_decoder_impl.h", + ], + deps = [ + "smtp_session_lib", + "//source/common/buffer:buffer_lib", + "//source/extensions/filters/network:well_known_names", + ], +) + +envoy_cc_library( + name = "smtp_session_lib", + srcs = ["smtp_session.cc"], + hdrs = [ + "smtp_command.h", + "smtp_decoder.h", + "smtp_handler.h", + "smtp_session.h", + ], + deps = [ + "smtp_transaction_lib", + "//source/common/buffer:buffer_lib", + "//source/extensions/filters/network:well_known_names", + ], +) + +envoy_cc_library( + name = "smtp_transaction_lib", + srcs = ["smtp_transaction.cc"], + hdrs = [ + "smtp_command.h", + "smtp_decoder.h", + "smtp_transaction.h", + ], + deps = [ + "smtp_utils_lib", + "//envoy/stream_info:stream_info_interface", + "//source/common/buffer:buffer_lib", + "//source/common/protobuf:utility_lib", + "//source/common/stream_info:stream_info_lib", + "//source/extensions/filters/network:well_known_names", + ], +) + +envoy_cc_library( + name = "smtp_utils_lib", + srcs = ["smtp_utils.cc"], + hdrs = ["smtp_utils.h"], + deps = [], +) + +envoy_cc_contrib_extension( + name = "config", + srcs = ["config.cc"], + hdrs = ["config.h"], + repository = "@envoy", + deps = [ + ":filter", + "//envoy/access_log:access_log_interface", + "//source/common/access_log:access_log_lib", + "//source/extensions/filters/network:well_known_names", + "//source/extensions/filters/network/common:factory_base_lib", + "@envoy_api//contrib/envoy/extensions/filters/network/smtp_proxy/v3alpha:pkg_cc_proto", + "@envoy_api//envoy/config/accesslog/v3:pkg_cc_proto", + ], +) diff --git a/contrib/smtp_proxy/filters/network/source/config.cc b/contrib/smtp_proxy/filters/network/source/config.cc new file mode 100644 index 000000000000..18685e51df85 --- /dev/null +++ b/contrib/smtp_proxy/filters/network/source/config.cc @@ -0,0 +1,48 @@ +#include "contrib/smtp_proxy/filters/network/source/config.h" + +#include "envoy/config/accesslog/v3/accesslog.pb.h" + +#include "source/common/access_log/access_log_impl.h" + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace SmtpProxy { + +/** + * Config registration for the SMTP Proxy filter. @see NamedNetworkFilterConfigFactory. + */ +Network::FilterFactoryCb +NetworkFilters::SmtpProxy::SmtpConfigFactory::createFilterFactoryFromProtoTyped( + const envoy::extensions::filters::network::smtp_proxy::v3alpha::SmtpProxy& proto_config, + Server::Configuration::FactoryContext& context) { + ASSERT(!proto_config.stat_prefix().empty()); + + SmtpFilterConfig::SmtpFilterConfigOptions config_options; + config_options.stats_prefix_ = fmt::format("smtp.{}", proto_config.stat_prefix()); + config_options.upstream_tls_ = proto_config.upstream_tls(); + config_options.tracing_ = proto_config.tracing(); + for (const envoy::config::accesslog::v3::AccessLog& log_config : proto_config.access_log()) { + config_options.access_logs_.emplace_back( + AccessLog::AccessLogFactory::fromProto(log_config, context)); + } + + SmtpFilterConfigSharedPtr filter_config( + std::make_shared(config_options, context.scope())); + + auto& time_source = context.mainThreadDispatcher().timeSource(); + return [filter_config, &time_source, &context](Network::FilterManager& filter_manager) -> void { + filter_manager.addFilter( + std::make_shared(filter_config, time_source, context.api().randomGenerator())); + }; +} + +/** + * Static registration for the SMTP Proxy filter. @see RegisterFactory. + */ +REGISTER_FACTORY(SmtpConfigFactory, Server::Configuration::NamedNetworkFilterConfigFactory); + +} // namespace SmtpProxy +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy diff --git a/contrib/smtp_proxy/filters/network/source/config.h b/contrib/smtp_proxy/filters/network/source/config.h new file mode 100644 index 000000000000..39a0fd956c45 --- /dev/null +++ b/contrib/smtp_proxy/filters/network/source/config.h @@ -0,0 +1,35 @@ +#pragma once + +#include "source/extensions/filters/network/common/factory_base.h" +#include "source/extensions/filters/network/well_known_names.h" + +#include "contrib/envoy/extensions/filters/network/smtp_proxy/v3alpha/smtp_proxy.pb.h" +#include "contrib/envoy/extensions/filters/network/smtp_proxy/v3alpha/smtp_proxy.pb.validate.h" +#include "contrib/smtp_proxy/filters/network/source/smtp_filter.h" + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace SmtpProxy { + +/** + * Config registration for the smtp proxy filter. @see NamedNetworkFilterConfigFactory. + */ + +class SmtpConfigFactory : public Common::FactoryBase< + envoy::extensions::filters::network::smtp_proxy::v3alpha::SmtpProxy> { +public: + SmtpConfigFactory() : FactoryBase{NetworkFilterNames::get().SmtpProxy} {} + +private: + Network::FilterFactoryCb createFilterFactoryFromProtoTyped( + const envoy::extensions::filters::network::smtp_proxy::v3alpha::SmtpProxy& proto_config, + Server::Configuration::FactoryContext& context) override; + + // std::vector access_logs_; +}; + +} // namespace SmtpProxy +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy diff --git a/contrib/smtp_proxy/filters/network/source/smtp_command.h b/contrib/smtp_proxy/filters/network/source/smtp_command.h new file mode 100644 index 000000000000..8ce48a73cd11 --- /dev/null +++ b/contrib/smtp_proxy/filters/network/source/smtp_command.h @@ -0,0 +1,54 @@ +#pragma once +#include +#include + +#include "source/common/common/logger.h" +#include "source/common/protobuf/utility.h" + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace SmtpProxy { + +class SmtpCommand { +public: + enum class Type { + None = 0, + NonTransactionCommand, + TransactionCommand, + Others, + }; + + SmtpCommand(const std::string& name, SmtpCommand::Type type, TimeSource& time_source) + : name_(name), type_(type), time_source_(time_source), + start_time_(time_source.monotonicTime()) {} + + SmtpCommand::Type getType() { return type_; } + std::string& getName() { return name_; } + + uint16_t& getResponseCode() { return response_code_; } + int64_t& getDuration() { return duration_; } + + void onComplete(std::string& response, uint16_t response_code) { + response_code_ = response_code; + response_ = response; + auto end_time_ = time_source_.monotonicTime(); + const auto response_time = + std::chrono::duration_cast(end_time_ - start_time_); + duration_ = response_time.count(); + } + +private: + std::string name_; + uint16_t response_code_{0}; + std::string response_; + SmtpCommand::Type type_{SmtpCommand::Type::None}; + TimeSource& time_source_; + const MonotonicTime start_time_; + int64_t duration_ = 0; +}; + +} // namespace SmtpProxy +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy diff --git a/contrib/smtp_proxy/filters/network/source/smtp_decoder.h b/contrib/smtp_proxy/filters/network/source/smtp_decoder.h new file mode 100644 index 000000000000..4e01ca20fc93 --- /dev/null +++ b/contrib/smtp_proxy/filters/network/source/smtp_decoder.h @@ -0,0 +1,55 @@ +#pragma once +#include + +#include "envoy/common/platform.h" +#include "envoy/network/connection.h" + +#include "source/common/buffer/buffer_impl.h" +#include "source/common/common/logger.h" + +#include "absl/container/flat_hash_map.h" +#include "contrib/smtp_proxy/filters/network/source/smtp_utils.h" + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace SmtpProxy { + +// General callbacks for dispatching decoded SMTP messages to a sink. +class DecoderCallbacks { +public: + virtual ~DecoderCallbacks() = default; + + virtual void incSmtpTransactions() PURE; + virtual void incSmtpTransactionsAborted() PURE; + virtual void incSmtpSessionRequests() PURE; + virtual void incSmtpConnectionEstablishmentErrors() PURE; + virtual void incSmtpSessionsCompleted() PURE; + virtual void incSmtpSessionsTerminated() PURE; + virtual void incTlsTerminatedSessions() PURE; + virtual void incTlsTerminationErrors() PURE; + virtual void incUpstreamTlsSuccess() PURE; + virtual void incUpstreamTlsFailed() PURE; + + virtual void incSmtpAuthErrors() PURE; + virtual void incMailDataTransferErrors() PURE; + virtual void incMailRcptErrors() PURE; + + virtual bool downstreamStartTls(absl::string_view) PURE; + virtual bool sendReplyDownstream(absl::string_view) PURE; + virtual bool upstreamTlsRequired() const PURE; + virtual bool tracingEnabled() PURE; + virtual bool upstreamStartTls() PURE; + virtual bool sendUpstream(Buffer::Instance&) PURE; + virtual Buffer::OwnedImpl& getReadBuffer() PURE; + virtual Buffer::OwnedImpl& getWriteBuffer() PURE; + virtual void closeDownstreamConnection() PURE; + virtual Network::Connection& connection() const PURE; + virtual StreamInfo::StreamInfo& getStreamInfo() PURE; + virtual void emitLogEntry(StreamInfo::StreamInfo&) PURE; +}; + +} // namespace SmtpProxy +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy diff --git a/contrib/smtp_proxy/filters/network/source/smtp_decoder_impl.cc b/contrib/smtp_proxy/filters/network/source/smtp_decoder_impl.cc new file mode 100644 index 000000000000..baada9742db1 --- /dev/null +++ b/contrib/smtp_proxy/filters/network/source/smtp_decoder_impl.cc @@ -0,0 +1,112 @@ +#include "contrib/smtp_proxy/filters/network/source/smtp_decoder_impl.h" + +#include "source/common/common/logger.h" +#include "source/extensions/filters/network/well_known_names.h" + +#include "absl/strings/match.h" + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace SmtpProxy { + +DecoderImpl::DecoderImpl(DecoderCallbacks* callbacks, TimeSource& time_source, + Random::RandomGenerator& random_generator) + : callbacks_(callbacks), time_source_(time_source), random_generator_(random_generator) { + session_ = new SmtpSession(callbacks, time_source_, random_generator_); +} + +SmtpUtils::Result DecoderImpl::onData(Buffer::Instance& data, bool upstream) { + SmtpUtils::Result result = SmtpUtils::Result::ReadyForNext; + if (upstream) { + result = parseResponse(data); + data.drain(data.length()); + return result; + } + result = parseCommand(data); + data.drain(data.length()); + return result; +} + +SmtpUtils::Result DecoderImpl::parseCommand(Buffer::Instance& data) { + ENVOY_LOG(debug, "smtp_proxy parseCommand: decoding {} bytes", data.length()); + + SmtpUtils::Result result = SmtpUtils::Result::ReadyForNext; + + if (getSession()->isTerminated()) { + return result; + } + if (getSession()->isDataTransferInProgress()) { + getSession()->updateBytesMeterOnCommand(data); + return result; + } + + std::string buffer = data.toString(); + ENVOY_LOG(debug, "received command {}", buffer); + // Each SMTP command ends with CRLF ("\r\n"), if received buffer doesn't end with CRLF, the filter + // will not process it. + if (!absl::EndsWith(buffer, SmtpUtils::smtpCrlfSuffix)) { + return result; + } + + buffer = StringUtil::cropRight(buffer, SmtpUtils::smtpCrlfSuffix); + int length = buffer.length(); + + std::string command = ""; + std::string args = ""; + if (length < 4) { + return result; + } else if (length == 4) { + command = buffer; + } else if (length > 4) { + // 4 letter command with some args after a space. i.e. cmd should have at least length=6 + if (length >= 6 && buffer[4] == ' ' && buffer[5] != ' ') { + command = buffer.substr(0, 4); + args = buffer.substr(5); + } else if (absl::EqualsIgnoreCase(buffer, SmtpUtils::startTlsCommand)) { + command = SmtpUtils::startTlsCommand; + } + } + result = session_->handleCommand(command, args); + return result; +} + +SmtpUtils::Result DecoderImpl::parseResponse(Buffer::Instance& data) { + ENVOY_LOG(debug, "smtp_proxy: decoding response {} bytes", data.length()); + + SmtpUtils::Result result = SmtpUtils::Result::ReadyForNext; + + // Special handling to parse any error response to connection request. + if (!(session_->isCommandInProgress()) && + session_->getState() != SmtpSession::State::ConnectionRequest) { + return result; + } + + std::string response = data.toString(); + if (!absl::EndsWith(response, SmtpUtils::smtpCrlfSuffix)) { + return result; + } + + response = StringUtil::cropRight(response, SmtpUtils::smtpCrlfSuffix); + int length = response.length(); + if (length < 3) { + // Minimum 3 byte response code needed to parse response from server. + return result; + } + std::string response_code_str = response.substr(0, 3); + uint16_t response_code = 0; + try { + response_code = stoi(response_code_str); + } catch (...) { + response_code = 0; + ENVOY_LOG(error, "smtp_proxy: error while decoding response code ", response_code); + } + result = session_->handleResponse(response_code, response); + + return result; +} + +} // namespace SmtpProxy +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy \ No newline at end of file diff --git a/contrib/smtp_proxy/filters/network/source/smtp_decoder_impl.h b/contrib/smtp_proxy/filters/network/source/smtp_decoder_impl.h new file mode 100644 index 000000000000..acb848477d08 --- /dev/null +++ b/contrib/smtp_proxy/filters/network/source/smtp_decoder_impl.h @@ -0,0 +1,54 @@ +#pragma once +#include + +#include "contrib/smtp_proxy/filters/network/source/smtp_decoder.h" +#include "contrib/smtp_proxy/filters/network/source/smtp_session.h" + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace SmtpProxy { + +// SMTP message decoder. +class Decoder { +public: + virtual ~Decoder() = default; + + virtual SmtpUtils::Result onData(Buffer::Instance& data, bool) PURE; + virtual SmtpUtils::Result parseCommand(Buffer::Instance& data) PURE; + virtual SmtpUtils::Result parseResponse(Buffer::Instance& data) PURE; + virtual SmtpSession* getSession() PURE; +}; + +using DecoderPtr = std::unique_ptr; + +class DecoderImpl : public Decoder, Logger::Loggable { +public: + DecoderImpl(DecoderCallbacks* callbacks, TimeSource& time_source, + Random::RandomGenerator& random_generator); + + ~DecoderImpl() { + delete session_; + session_ = nullptr; + } + void setSession(SmtpSession* session) { session_ = session; } + SmtpUtils::Result onData(Buffer::Instance& data, bool upstream) override; + SmtpSession* getSession() override { return session_; } + SmtpUtils::Result parseCommand(Buffer::Instance& data) override; + SmtpUtils::Result parseResponse(Buffer::Instance& data) override; + +protected: + SmtpSession* session_; + DecoderCallbacks* callbacks_{}; + Buffer::OwnedImpl response_; + Buffer::OwnedImpl last_response_; + Buffer::OwnedImpl response_on_hold_; + + TimeSource& time_source_; + Random::RandomGenerator& random_generator_; +}; + +} // namespace SmtpProxy +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy \ No newline at end of file diff --git a/contrib/smtp_proxy/filters/network/source/smtp_filter.cc b/contrib/smtp_proxy/filters/network/source/smtp_filter.cc new file mode 100644 index 000000000000..b6c2dab5bbdc --- /dev/null +++ b/contrib/smtp_proxy/filters/network/source/smtp_filter.cc @@ -0,0 +1,192 @@ +#include "contrib/smtp_proxy/filters/network/source/smtp_filter.h" + +#include +#include + +#include "envoy/buffer/buffer.h" +#include "envoy/network/connection.h" + +#include "source/common/common/assert.h" + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace SmtpProxy { + +SmtpFilterConfig::SmtpFilterConfig(const SmtpFilterConfigOptions& config_options, + Stats::Scope& scope) + : scope_{scope}, stats_(generateStats(config_options.stats_prefix_, scope)), + tracing_(config_options.tracing_), upstream_tls_(config_options.upstream_tls_) { + access_logs_ = config_options.access_logs_; +} + +SmtpFilter::SmtpFilter(SmtpFilterConfigSharedPtr config, TimeSource& time_source, + Random::RandomGenerator& random_generator) + : config_{config}, time_source_(time_source), random_generator_(random_generator) {} + +void SmtpFilter::emitLogEntry(StreamInfo::StreamInfo& stream_info) { + for (const auto& access_log : config_->accessLogs()) { + access_log->log(nullptr, nullptr, nullptr, stream_info); + } +} + +Network::FilterStatus SmtpFilter::onNewConnection() { + incSmtpSessionRequests(); + if (!decoder_) { + decoder_ = createDecoder(this, time_source_, random_generator_); + } + return Network::FilterStatus::Continue; +} + +bool SmtpFilter::downstreamStartTls(absl::string_view response) { + Buffer::OwnedImpl buffer; + buffer.add(response); + + read_callbacks_->connection().addBytesSentCallback([=](uint64_t bytes) -> bool { + // Wait until response has been sent. + if (bytes >= response.length()) { + if (!read_callbacks_->connection().startSecureTransport()) { + ENVOY_CONN_LOG(trace, "smtp_proxy filter: cannot switch to tls", + read_callbacks_->connection(), bytes); + return true; + } else { + // Switch to TLS has been completed. + ENVOY_CONN_LOG(trace, "smtp_proxy filter: switched to tls", read_callbacks_->connection(), + bytes); + return false; + } + } + return true; + }); + + read_callbacks_->connection().write(buffer, false); + return false; +} + +bool SmtpFilter::sendUpstream(Buffer::Instance& data) { + read_callbacks_->injectReadDataToFilterChain(data, false); + return true; +} + +bool SmtpFilter::sendReplyDownstream(absl::string_view response) { + Buffer::OwnedImpl buffer; + buffer.add(response); + if (read_callbacks_->connection().state() != Network::Connection::State::Open) { + ENVOY_LOG(warn, "downstream connection is closed or closing"); + return true; + } + read_callbacks_->connection().addBytesSentCallback([=](uint64_t bytes) -> bool { + // Wait until response has been sent. + if (bytes >= response.length()) { + return false; + } + return true; + }); + + read_callbacks_->connection().write(buffer, false); + return false; +} + +bool SmtpFilter::upstreamTlsRequired() const { + return (config_->upstream_tls_ == + envoy::extensions::filters::network::smtp_proxy::v3alpha::SmtpProxy::REQUIRE); +} + +bool SmtpFilter::tracingEnabled() { return config_->tracing_; } + +bool SmtpFilter::upstreamStartTls() { + // Try to switch upstream connection to use a secure channel. + if (read_callbacks_->startUpstreamSecureTransport()) { + config_->stats_.sessions_upstream_tls_success_.inc(); + ENVOY_CONN_LOG(trace, "smtp_proxy: upstream TLS enabled.", read_callbacks_->connection()); + } else { + ENVOY_CONN_LOG(info, + "smtp_proxy: cannot enable upstream secure transport. Check " + "configuration. Terminating.", + read_callbacks_->connection()); + config_->stats_.sessions_upstream_tls_failed_.inc(); + return false; + } + return true; +} + +void SmtpFilter::incSmtpTransactions() { config_->stats_.smtp_transactions_.inc(); } + +void SmtpFilter::incSmtpTransactionsAborted() { config_->stats_.smtp_transactions_aborted_.inc(); } +void SmtpFilter::incSmtpSessionRequests() { config_->stats_.smtp_session_requests_.inc(); } +void SmtpFilter::incSmtpSessionsCompleted() { config_->stats_.smtp_sessions_completed_.inc(); } + +void SmtpFilter::incSmtpSessionsTerminated() { config_->stats_.smtp_sessions_terminated_.inc(); } + +void SmtpFilter::incSmtpConnectionEstablishmentErrors() { + config_->stats_.smtp_connection_establishment_errors_.inc(); +} + +void SmtpFilter::incMailDataTransferErrors() { + config_->stats_.smtp_mail_data_transfer_errors_.inc(); +} +void SmtpFilter::incMailRcptErrors() { config_->stats_.smtp_rcpt_errors_.inc(); } + +void SmtpFilter::incTlsTerminatedSessions() { config_->stats_.smtp_tls_terminated_sessions_.inc(); } + +void SmtpFilter::incTlsTerminationErrors() { config_->stats_.smtp_tls_termination_errors_.inc(); } + +void SmtpFilter::incSmtpAuthErrors() { config_->stats_.smtp_auth_errors_.inc(); } + +void SmtpFilter::incUpstreamTlsSuccess() { config_->stats_.sessions_upstream_tls_success_.inc(); } + +void SmtpFilter::incUpstreamTlsFailed() { config_->stats_.sessions_upstream_tls_failed_.inc(); } + +void SmtpFilter::closeDownstreamConnection() { + read_callbacks_->connection().close(Network::ConnectionCloseType::NoFlush); + incSmtpSessionsTerminated(); +} + +// onData method processes payloads sent by downstream client. +Network::FilterStatus SmtpFilter::onData(Buffer::Instance& data, bool end_stream) { + ENVOY_CONN_LOG(trace, "smtp_proxy: got {} bytes", read_callbacks_->connection(), data.length(), + "end_stream ", end_stream); + read_buffer_.add(data); + Network::FilterStatus result = doDecode(read_buffer_, false); + if (result == Network::FilterStatus::StopIteration) { + ASSERT(read_buffer_.length() == 0); + data.drain(data.length()); + } + return result; +} + +// onWrite method processes payloads sent by upstream to the client. +Network::FilterStatus SmtpFilter::onWrite(Buffer::Instance& data, bool end_stream) { + ENVOY_CONN_LOG(trace, "smtp_proxy: got {} bytes", write_callbacks_->connection(), data.length(), + "end_stream ", end_stream); + write_buffer_.add(data); + Network::FilterStatus result = doDecode(write_buffer_, true); + if (result == Network::FilterStatus::StopIteration) { + ASSERT(write_buffer_.length() == 0); + data.drain(data.length()); + } + return result; +} + +Network::FilterStatus SmtpFilter::doDecode(Buffer::Instance& data, bool upstream) { + + switch (decoder_->onData(data, upstream)) { + case SmtpUtils::Result::ReadyForNext: + return Network::FilterStatus::Continue; + case SmtpUtils::Result::Stopped: + return Network::FilterStatus::StopIteration; + default: + break; + } + return Network::FilterStatus::Continue; +} + +DecoderPtr SmtpFilter::createDecoder(DecoderCallbacks* callbacks, TimeSource& time_source, + Random::RandomGenerator& random_generator) { + return std::make_unique(callbacks, time_source, random_generator); +} + +} // namespace SmtpProxy +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy \ No newline at end of file diff --git a/contrib/smtp_proxy/filters/network/source/smtp_filter.h b/contrib/smtp_proxy/filters/network/source/smtp_filter.h new file mode 100644 index 000000000000..4852c0b594f7 --- /dev/null +++ b/contrib/smtp_proxy/filters/network/source/smtp_filter.h @@ -0,0 +1,152 @@ +#pragma once + +#include "envoy/access_log/access_log.h" +#include "envoy/buffer/buffer.h" +#include "envoy/common/random_generator.h" +#include "envoy/network/filter.h" +#include "envoy/stats/scope.h" +#include "envoy/stats/stats.h" +#include "envoy/stats/stats_macros.h" + +#include "source/common/common/logger.h" +#include "source/common/common/utility.h" + +#include "contrib/envoy/extensions/filters/network/smtp_proxy/v3alpha/smtp_proxy.pb.h" +#include "contrib/smtp_proxy/filters/network/source/smtp_decoder_impl.h" + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace SmtpProxy { + +/** + * All SMTP proxy stats. @see stats_macros.h + */ +#define ALL_SMTP_PROXY_STATS(COUNTER) \ + COUNTER(smtp_session_requests) \ + COUNTER(smtp_connection_establishment_errors) \ + COUNTER(smtp_sessions_completed) \ + COUNTER(smtp_sessions_terminated) \ + COUNTER(smtp_transactions) \ + COUNTER(smtp_transactions_aborted) \ + COUNTER(smtp_tls_terminated_sessions) \ + COUNTER(smtp_tls_termination_errors) \ + COUNTER(sessions_upstream_tls_success) \ + COUNTER(sessions_upstream_tls_failed) \ + COUNTER(smtp_auth_errors) \ + COUNTER(smtp_mail_data_transfer_errors) \ + COUNTER(smtp_rcpt_errors) + +/** + * Struct definition for all SMTP proxy stats. @see stats_macros.h + */ +struct SmtpProxyStats { + ALL_SMTP_PROXY_STATS(GENERATE_COUNTER_STRUCT) +}; + +class SmtpFilterConfig { +public: + struct SmtpFilterConfigOptions { + std::string stats_prefix_; + bool tracing_; + envoy::extensions::filters::network::smtp_proxy::v3alpha::SmtpProxy::UpstreamTLSMode + upstream_tls_; + std::vector access_logs_; + }; + SmtpFilterConfig(const SmtpFilterConfigOptions& config_options, Stats::Scope& scope); + const SmtpProxyStats& stats() { return stats_; } + const std::vector& accessLogs() const { return access_logs_; } + Stats::Scope& scope_; + SmtpProxyStats stats_; + bool tracing_; + std::vector access_logs_; + envoy::extensions::filters::network::smtp_proxy::v3alpha::SmtpProxy::UpstreamTLSMode + upstream_tls_{envoy::extensions::filters::network::smtp_proxy::v3alpha::SmtpProxy::DISABLE}; + +private: + SmtpProxyStats generateStats(const std::string& prefix, Stats::Scope& scope) { + return SmtpProxyStats{ALL_SMTP_PROXY_STATS(POOL_COUNTER_PREFIX(scope, prefix))}; + } +}; + +using SmtpFilterConfigSharedPtr = std::shared_ptr; + +class SmtpFilter : public Network::Filter, DecoderCallbacks, Logger::Loggable { +public: + // Network::ReadFilter + SmtpFilter(SmtpFilterConfigSharedPtr config, TimeSource& time_source, + Random::RandomGenerator& random_generator); + ~SmtpFilter() { + // delete session_; + // session_ = nullptr; + if (config_->accessLogs().size() > 0) { + emitLogEntry(getStreamInfo()); + } + } + Network::FilterStatus onData(Buffer::Instance& data, bool end_stream) override; + Network::FilterStatus onNewConnection() override; + void initializeReadFilterCallbacks(Network::ReadFilterCallbacks& callbacks) override { + read_callbacks_ = &callbacks; + } + + // Network::WriteFilter + Network::FilterStatus onWrite(Buffer::Instance& data, bool end_stream) override; + void initializeWriteFilterCallbacks(Network::WriteFilterCallbacks& callbacks) override { + write_callbacks_ = &callbacks; + } + Network::FilterStatus doDecode(Buffer::Instance& buffer, bool upstream); + DecoderPtr createDecoder(DecoderCallbacks* callbacks, TimeSource& time_source, + Random::RandomGenerator&); + + const SmtpProxyStats& getStats() const { return config_->stats_; } + + Network::Connection& connection() const override { return read_callbacks_->connection(); } + const SmtpFilterConfigSharedPtr& getConfig() const { return config_; } + + void incSmtpSessionRequests() override; + void incSmtpConnectionEstablishmentErrors() override; + void incTlsTerminatedSessions() override; + void incSmtpTransactions() override; + void incSmtpTransactionsAborted() override; + void incSmtpSessionsCompleted() override; + void incSmtpSessionsTerminated() override; + void incTlsTerminationErrors() override; + void incUpstreamTlsSuccess() override; + void incUpstreamTlsFailed() override; + + void incSmtpAuthErrors() override; + void incMailDataTransferErrors() override; + void incMailRcptErrors() override; + + bool downstreamStartTls(absl::string_view response) override; + bool sendReplyDownstream(absl::string_view response) override; + bool upstreamTlsRequired() const override; + bool tracingEnabled() override; + bool sendUpstream(Buffer::Instance& buffer) override; + + bool upstreamStartTls() override; + void closeDownstreamConnection() override; + SmtpSession* getSession() { return decoder_->getSession(); } + Buffer::OwnedImpl& getReadBuffer() override { return read_buffer_; } + Buffer::OwnedImpl& getWriteBuffer() override { return write_buffer_; } + void emitLogEntry(StreamInfo::StreamInfo& stream_info) override; + StreamInfo::StreamInfo& getStreamInfo() override { + return read_callbacks_->connection().streamInfo(); + } + +private: + Network::ReadFilterCallbacks* read_callbacks_{}; + Network::WriteFilterCallbacks* write_callbacks_{}; + + SmtpFilterConfigSharedPtr config_; + Buffer::OwnedImpl read_buffer_; + Buffer::OwnedImpl write_buffer_; + std::unique_ptr decoder_; + TimeSource& time_source_; + Random::RandomGenerator& random_generator_; +}; + +} // namespace SmtpProxy +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy \ No newline at end of file diff --git a/contrib/smtp_proxy/filters/network/source/smtp_handler.h b/contrib/smtp_proxy/filters/network/source/smtp_handler.h new file mode 100644 index 000000000000..062ece1815a3 --- /dev/null +++ b/contrib/smtp_proxy/filters/network/source/smtp_handler.h @@ -0,0 +1,34 @@ +#pragma once +#include + +#include "envoy/common/platform.h" +#include "envoy/network/connection.h" + +#include "source/common/buffer/buffer_impl.h" +#include "source/common/common/logger.h" + +#include "absl/container/flat_hash_map.h" +#include "contrib/smtp_proxy/filters/network/source/smtp_utils.h" + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace SmtpProxy { + +class SmtpHandler { +public: + virtual ~SmtpHandler() = default; + + virtual bool isTerminated() PURE; + virtual bool isDataTransferInProgress() PURE; + virtual bool isCommandInProgress() PURE; + virtual SmtpUtils::Result handleCommand(std::string& command, std::string& args) PURE; + virtual SmtpUtils::Result handleResponse(uint16_t& response_code, std::string& response) PURE; + + virtual void updateBytesMeterOnCommand(Buffer::Instance& data) PURE; +}; + +} // namespace SmtpProxy +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy diff --git a/contrib/smtp_proxy/filters/network/source/smtp_session.cc b/contrib/smtp_proxy/filters/network/source/smtp_session.cc new file mode 100644 index 000000000000..f77738b61387 --- /dev/null +++ b/contrib/smtp_proxy/filters/network/source/smtp_session.cc @@ -0,0 +1,625 @@ +#include "contrib/smtp_proxy/filters/network/source/smtp_session.h" + +#include "source/common/buffer/buffer_impl.h" +#include "source/extensions/filters/network/well_known_names.h" + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace SmtpProxy { + +SmtpSession::SmtpSession(DecoderCallbacks* callbacks, TimeSource& time_source, + Random::RandomGenerator& random_generator) + : callbacks_(callbacks), time_source_(time_source), random_generator_(random_generator) {} +void SmtpSession::newCommand(const std::string& name, SmtpCommand::Type type) { + current_command_ = std::make_shared(name, type, time_source_); + command_in_progress_ = true; +} + +void SmtpSession::createNewTransaction() { + if (smtp_transaction_ == nullptr) { + smtp_transaction_ = + new SmtpTransaction(session_id_, callbacks_, time_source_, random_generator_); + transaction_in_progress_ = true; + } +} + +void SmtpSession::updateBytesMeterOnCommand(Buffer::Instance& data) { + + if (data.length() == 0) + return; + + if (transaction_in_progress_) { + getTransaction()->getStreamInfo().addBytesReceived(data.length()); + getTransaction()->getStreamInfo().getDownstreamBytesMeter()->addWireBytesReceived( + data.length()); + getTransaction()->getStreamInfo().getUpstreamBytesMeter()->addHeaderBytesSent(data.length()); + + if (isDataTransferInProgress()) { + getTransaction()->addPayloadBytes(data.length()); + } + } +} + +void SmtpSession::updateBytesMeterOnResponse(Buffer::Instance& data) { + if (data.length() == 0) + return; + + if (transaction_in_progress_) { + getTransaction()->getStreamInfo().addBytesSent(data.length()); + getTransaction()->getStreamInfo().getDownstreamBytesMeter()->addWireBytesSent(data.length()); + getTransaction()->getStreamInfo().getUpstreamBytesMeter()->addWireBytesReceived(data.length()); + } +} + +SmtpUtils::Result SmtpSession::handleCommand(std::string& command, std::string& args) { + + SmtpUtils::Result result = SmtpUtils::Result::ReadyForNext; + if (command == "") + return result; + + if (absl::EqualsIgnoreCase(command, SmtpUtils::smtpEhloCommand) || + absl::EqualsIgnoreCase(command, SmtpUtils::smtpHeloCommand)) { + result = handleEhlo(command); + } else if (absl::EqualsIgnoreCase(command, SmtpUtils::smtpMailCommand)) { + result = handleMail(args); + } else if (absl::EqualsIgnoreCase(command, SmtpUtils::smtpRcptCommand)) { + result = handleRcpt(args); + } else if (absl::EqualsIgnoreCase(command, SmtpUtils::smtpDataCommand)) { + result = handleData(args); + } else if (absl::EqualsIgnoreCase(command, SmtpUtils::smtpAuthCommand)) { + result = handleAuth(); + } else if (absl::EqualsIgnoreCase(command, SmtpUtils::startTlsCommand)) { + result = handleStarttls(); + } else if (absl::EqualsIgnoreCase(command, SmtpUtils::smtpRsetCommand)) { + result = handleReset(args); + } else if (absl::EqualsIgnoreCase(command, SmtpUtils::smtpQuitCommand)) { + result = handleQuit(args); + } else { + result = handleOtherCmds(command); + } + return result; +} + +SmtpUtils::Result SmtpSession::handleEhlo(std::string& command) { + SmtpUtils::Result result = SmtpUtils::Result::ReadyForNext; + + newCommand(StringUtil::toUpper(command), SmtpCommand::Type::NonTransactionCommand); + setState(SmtpSession::State::SessionInitRequest); + + return result; +} + +SmtpUtils::Result SmtpSession::handleMail(std::string& arg) { + SmtpUtils::Result result = SmtpUtils::Result::ReadyForNext; + + if (state_ != SmtpSession::State::SessionInProgress) { + callbacks_->sendReplyDownstream( + SmtpUtils::generateResponse(502, {5, 5, 1}, "Please introduce yourself first")); + result = SmtpUtils::Result::Stopped; + return result; + } + + if (arg.length() < 6 || !absl::EqualsIgnoreCase(arg.substr(0, 5), "FROM:")) { + callbacks_->sendReplyDownstream( + SmtpUtils::generateResponse(501, {5, 5, 2}, "Bad MAIL arg syntax of FROM:
")); + result = SmtpUtils::Result::Stopped; + return result; + } + newCommand(SmtpUtils::smtpMailCommand, SmtpCommand::Type::TransactionCommand); + createNewTransaction(); + std::string sender = SmtpUtils::extractAddress(arg); + getTransaction()->setSender(sender); + updateBytesMeterOnCommand(callbacks_->getReadBuffer()); + setTransactionState(SmtpTransaction::State::TransactionRequest); + return result; +} + +SmtpUtils::Result SmtpSession::handleRcpt(std::string& arg) { + SmtpUtils::Result result = SmtpUtils::Result::ReadyForNext; + + if (getTransaction() == nullptr) { + callbacks_->sendReplyDownstream( + SmtpUtils::generateResponse(502, {5, 5, 1}, "Missing MAIL FROM command")); + result = SmtpUtils::Result::Stopped; + return result; + } + + if (arg.length() <= 3 || !absl::EqualsIgnoreCase(arg.substr(0, 3), "TO:")) { + callbacks_->sendReplyDownstream( + SmtpUtils::generateResponse(501, {5, 5, 2}, "Bad RCPT arg syntax of TO:
")); + result = SmtpUtils::Result::Stopped; + return result; + } + + newCommand(SmtpUtils::smtpRcptCommand, SmtpCommand::Type::TransactionCommand); + std::string recipient = SmtpUtils::extractAddress(arg); + getTransaction()->addRcpt(recipient); + updateBytesMeterOnCommand(callbacks_->getReadBuffer()); + setTransactionState(SmtpTransaction::State::RcptCommand); + return result; +} + +SmtpUtils::Result SmtpSession::handleData(std::string& arg) { + SmtpUtils::Result result = SmtpUtils::Result::ReadyForNext; + + if (arg.length() > 0) { + callbacks_->sendReplyDownstream( + SmtpUtils::generateResponse(501, {5, 5, 4}, "No params allowed for DATA command")); + result = SmtpUtils::Result::Stopped; + return result; + } + + if (getTransaction() == nullptr || getTransaction()->getNoOfRecipients() == 0) { + callbacks_->sendReplyDownstream( + SmtpUtils::generateResponse(502, {5, 5, 1}, "Missing RCPT TO command")); + result = SmtpUtils::Result::Stopped; + return result; + } + + newCommand(SmtpUtils::smtpDataCommand, SmtpCommand::Type::TransactionCommand); + updateBytesMeterOnCommand(callbacks_->getReadBuffer()); + setDataTransferInProgress(true); + setTransactionState(SmtpTransaction::State::MailDataTransferRequest); + return result; +} + +SmtpUtils::Result SmtpSession::handleAuth() { + SmtpUtils::Result result = SmtpUtils::Result::ReadyForNext; + + if (state_ != SmtpSession::State::SessionInProgress) { + callbacks_->sendReplyDownstream( + SmtpUtils::generateResponse(502, {5, 5, 1}, "Please introduce yourself first")); + result = SmtpUtils::Result::Stopped; + return result; + } + + if (isAuthenticated()) { + callbacks_->sendReplyDownstream( + SmtpUtils::generateResponse(502, {5, 5, 1}, "Already authenticated")); + result = SmtpUtils::Result::Stopped; + return result; + } + + newCommand(SmtpUtils::smtpAuthCommand, SmtpCommand::Type::NonTransactionCommand); + setState(SmtpSession::State::SessionAuthRequest); + return result; +} + +SmtpUtils::Result SmtpSession::handleStarttls() { + SmtpUtils::Result result = SmtpUtils::Result::ReadyForNext; + + if (isSessionEncrypted()) { + callbacks_->sendReplyDownstream( + SmtpUtils::generateResponse(502, {5, 5, 1}, "Already running in TLS")); + result = SmtpUtils::Result::Stopped; + return result; + } + newCommand(SmtpUtils::startTlsCommand, SmtpCommand::Type::NonTransactionCommand); + if (callbacks_->upstreamTlsRequired()) { + // Send STARTTLS request to upstream. + setState(SmtpSession::State::UpstreamTlsNegotiation); + } else { + // Perform downstream TLS negotiation. + handleDownstreamTls(); + result = SmtpUtils::Result::Stopped; + } + return result; +} + +SmtpUtils::Result SmtpSession::handleReset(std::string& arg) { + SmtpUtils::Result result = SmtpUtils::Result::ReadyForNext; + + if (arg.length() > 0) { + callbacks_->sendReplyDownstream( + SmtpUtils::generateResponse(501, {5, 5, 4}, "No params allowed for RSET command")); + result = SmtpUtils::Result::Stopped; + return result; + } + + newCommand(SmtpUtils::smtpRsetCommand, SmtpCommand::Type::NonTransactionCommand); + if (getTransaction() != nullptr) { + setTransactionState(SmtpTransaction::State::TransactionAbortRequest); + } + return result; +} + +SmtpUtils::Result SmtpSession::handleQuit(std::string& arg) { + SmtpUtils::Result result = SmtpUtils::Result::ReadyForNext; + + if (arg.length() > 0) { + callbacks_->sendReplyDownstream( + SmtpUtils::generateResponse(501, {5, 5, 4}, "No params allowed for QUIT command")); + result = SmtpUtils::Result::Stopped; + return result; + } + newCommand(SmtpUtils::smtpQuitCommand, SmtpCommand::Type::NonTransactionCommand); + setState(SmtpSession::State::SessionTerminationRequest); + if (getTransaction() != nullptr) { + setTransactionState(SmtpTransaction::State::TransactionAbortRequest); + } + return result; +} + +SmtpUtils::Result SmtpSession::handleOtherCmds(std::string& command) { + SmtpUtils::Result result = SmtpUtils::Result::ReadyForNext; + newCommand(StringUtil::toUpper(command), SmtpCommand::Type::NonTransactionCommand); + return result; +} + +SmtpUtils::Result SmtpSession::handleResponse(uint16_t& response_code, std::string& response) { + SmtpUtils::Result result = SmtpUtils::Result::ReadyForNext; + // Special handling to parse any error response to connection request. + if (getState() == SmtpSession::State::ConnectionRequest) { + return handleConnResponse(response_code, response); + } + if (!isCommandInProgress()) { + return result; + } + std::string command = getCurrentCommand()->getName(); + + if (absl::EqualsIgnoreCase(command, SmtpUtils::smtpEhloCommand) || + absl::EqualsIgnoreCase(command, SmtpUtils::smtpHeloCommand)) { + result = handleEhloResponse(response_code, response); + } else if (absl::EqualsIgnoreCase(command, SmtpUtils::smtpMailCommand)) { + result = handleMailResponse(response_code, response); + } else if (absl::EqualsIgnoreCase(command, SmtpUtils::smtpRcptCommand)) { + result = handleRcptResponse(response_code, response); + } else if (absl::EqualsIgnoreCase(command, SmtpUtils::smtpDataCommand)) { + result = handleDataResponse(response_code, response); + } else if (absl::EqualsIgnoreCase(command, SmtpUtils::smtpAuthCommand)) { + result = handleAuthResponse(response_code, response); + } else if (absl::EqualsIgnoreCase(command, SmtpUtils::startTlsCommand)) { + result = handleStarttlsResponse(response_code, response); + } else if (absl::EqualsIgnoreCase(command, SmtpUtils::smtpRsetCommand)) { + result = handleResetResponse(response_code, response); + } else if (absl::EqualsIgnoreCase(command, SmtpUtils::smtpQuitCommand)) { + result = handleQuitResponse(response_code, response); + } else if (absl::EqualsIgnoreCase(command, SmtpUtils::xReqIdCommand)) { + result = handleXReqIdResponse(response_code, response); + } else { + result = handleOtherResponse(response_code, response); + } + + return result; +} + +SmtpUtils::Result SmtpSession::handleConnResponse(uint16_t& response_code, std::string& response) { + SmtpUtils::Result result = SmtpUtils::Result::ReadyForNext; + auto stream_id_provider = callbacks_->getStreamInfo().getStreamIdProvider(); + if (stream_id_provider.has_value()) { + session_id_ = stream_id_provider->toStringView().value_or(""); + } + + if (response_code == 220) { + setState(SmtpSession::State::ConnectionSuccess); + if (callbacks_->tracingEnabled() && !isXReqIdSent()) { + response_on_hold_ = response + SmtpUtils::smtpCrlfSuffix; + std::string x_req_id = SmtpUtils::xReqIdCommand + session_id_ + "\r\n"; + Buffer::OwnedImpl data(x_req_id); + newCommand(SmtpUtils::xReqIdCommand, SmtpCommand::Type::NonTransactionCommand); + callbacks_->sendUpstream(data); + setState(SmtpSession::State::XReqIdTransfer); + result = SmtpUtils::Result::Stopped; + return result; + } + } else if (response_code == 554) { + callbacks_->incSmtpConnectionEstablishmentErrors(); + } + return result; +} + +SmtpUtils::Result SmtpSession::handleEhloResponse(uint16_t& response_code, std::string& response) { + SmtpUtils::Result result = SmtpUtils::Result::ReadyForNext; + if (response_code == 250) { + setState(SmtpSession::State::SessionInProgress); + if (transaction_in_progress_) { + // Abort this transaction. + abortTransaction(); + } + } else { + setState(SmtpSession::State::ConnectionSuccess); + } + + storeResponse(response, response_code); + return result; +} + +SmtpUtils::Result SmtpSession::handleMailResponse(uint16_t& response_code, std::string& response) { + SmtpUtils::Result result = SmtpUtils::Result::ReadyForNext; + storeResponse(response, response_code); + updateBytesMeterOnResponse(callbacks_->getWriteBuffer()); + if (response_code == 250) { + setTransactionState(SmtpTransaction::State::TransactionInProgress); + if (callbacks_->tracingEnabled() && !getTransaction()->isXReqIdSent()) { + response_on_hold_ = response + SmtpUtils::smtpCrlfSuffix; + std::string x_req_id = + SmtpUtils::xReqIdCommand + getTransaction()->getTransactionId() + "\r\n"; + Buffer::OwnedImpl data(x_req_id); + newCommand(SmtpUtils::xReqIdCommand, SmtpCommand::Type::NonTransactionCommand); + updateBytesMeterOnCommand(data); + callbacks_->sendUpstream(data); + setTransactionState(SmtpTransaction::State::XReqIdTransfer); + result = SmtpUtils::Result::Stopped; + return result; + } + } else { + // error response to mail command + setTransactionState(SmtpTransaction::State::None); + } + + return result; +} + +SmtpUtils::Result SmtpSession::handleXReqIdResponse(uint16_t& response_code, + std::string& response) { + SmtpUtils::Result result = SmtpUtils::Result::ReadyForNext; + storeResponse(response, response_code); + if (transaction_in_progress_) { + updateBytesMeterOnResponse(callbacks_->getWriteBuffer()); + setTransactionState(SmtpTransaction::State::TransactionInProgress); + getTransaction()->setXReqIdSent(true); + } else { + x_req_id_sent_ = true; + setState(SmtpSession::State::ConnectionSuccess); + } + + callbacks_->sendReplyDownstream(getResponseOnHold()); + result = SmtpUtils::Result::Stopped; + return result; +} + +SmtpUtils::Result SmtpSession::handleRcptResponse(uint16_t& response_code, std::string& response) { + SmtpUtils::Result result = SmtpUtils::Result::ReadyForNext; + + if (response_code == 250 || response_code == 251) { + setTransactionState(SmtpTransaction::State::TransactionInProgress); + } else if (response_code >= 400 && response_code <= 599) { + callbacks_->incMailRcptErrors(); + } + storeResponse(response, response_code); + updateBytesMeterOnResponse(callbacks_->getWriteBuffer()); + return result; +} + +SmtpUtils::Result SmtpSession::handleDataResponse(uint16_t& response_code, std::string& response) { + SmtpUtils::Result result = SmtpUtils::Result::ReadyForNext; + storeResponse(response, response_code); + updateBytesMeterOnResponse(callbacks_->getWriteBuffer()); + if (response_code == 250) { + setTransactionState(SmtpTransaction::State::TransactionCompleted); + getTransaction()->setStatus(SmtpUtils::statusSuccess); + getSessionStats().transactions_completed++; + } else if (response_code == 354) { + // Intermediate response. + return result; + } else if (response_code >= 400 && response_code <= 599) { + callbacks_->incMailDataTransferErrors(); + setTransactionState(SmtpTransaction::State::None); + getTransaction()->setStatus(SmtpUtils::statusFailed); + getSessionStats().transactions_failed++; + } + onTransactionComplete(); + setDataTransferInProgress(false); + return result; +} + +SmtpUtils::Result SmtpSession::handleResetResponse(uint16_t& response_code, std::string& response) { + SmtpUtils::Result result = SmtpUtils::Result::ReadyForNext; + storeResponse(response, response_code); + + if (transaction_in_progress_) { + if (response_code == 250) { + abortTransaction(); + } else { + setTransactionState(SmtpTransaction::State::TransactionInProgress); + } + } + + return result; +} + +SmtpUtils::Result SmtpSession::handleQuitResponse(uint16_t& response_code, std::string& response) { + SmtpUtils::Result result = SmtpUtils::Result::ReadyForNext; + storeResponse(response, response_code); + if (response_code == 221) { + terminateSession(); + } else { + setState(SmtpSession::State::SessionInProgress); + } + return result; +} + +SmtpUtils::Result SmtpSession::handleAuthResponse(uint16_t& response_code, std::string& response) { + SmtpUtils::Result result = SmtpUtils::Result::ReadyForNext; + + storeResponse(response, response_code); + if (response_code == 334) { + return result; + } else if (response_code >= 400 && response_code <= 599) { + callbacks_->incSmtpAuthErrors(); + } else if (response_code >= 200 && response_code <= 299) { + setAuthStatus(true); + } + setState(SmtpSession::State::SessionInProgress); + return result; +} + +SmtpUtils::Result SmtpSession::handleStarttlsResponse(uint16_t& response_code, + std::string& response) { + SmtpUtils::Result result = SmtpUtils::Result::ReadyForNext; + + if (getState() == SmtpSession::State::UpstreamTlsNegotiation) { + if (response_code == 220) { + if (callbacks_->upstreamStartTls()) { + // Upstream TLS connection established.Now encrypt downstream connection. + upstream_session_type_ = SmtpUtils::SessionType::Tls; + handleDownstreamTls(); + result = SmtpUtils::Result::Stopped; + return result; + } + } + // We terminate this session if upstream server does not support TLS i.e. response code != 220 + // or if TLS handshake error occured with upstream + storeResponse(response, response_code); + terminateSession(); + callbacks_->sendReplyDownstream( + SmtpUtils::generateResponse(502, {5, 5, 1}, "TLS not supported")); + callbacks_->closeDownstreamConnection(); + result = SmtpUtils::Result::Stopped; + return result; + } + storeResponse(response, response_code); + return result; +} + +SmtpUtils::Result SmtpSession::handleOtherResponse(uint16_t& response_code, std::string& response) { + SmtpUtils::Result result = SmtpUtils::Result::ReadyForNext; + + storeResponse(response, response_code); + return result; +} + +SmtpUtils::Result SmtpSession::storeResponse(std::string response, uint16_t response_code) { + + SmtpUtils::Result result = SmtpUtils::Result::ReadyForNext; + + if (current_command_ == nullptr) + return result; + + // current_command_->storeResponse(response, response_code); + current_command_->onComplete(response, response_code); + + if (response_code / 100 == 3) { + // If intermediate response code, do not end current command processing + return result; + } + + command_in_progress_ = false; + session_stats_.total_commands++; + switch (current_command_->getType()) { + case SmtpCommand::Type::NonTransactionCommand: { + session_commands_.push_back(current_command_); + break; + } + case SmtpCommand::Type::TransactionCommand: { + getTransaction()->addTrxnCommand(current_command_); + break; + } + default: + break; + }; + return result; +} + +void SmtpSession::encode(ProtobufWkt::Struct& metadata) { + + auto& fields = *(metadata.mutable_fields()); + + // TODO: store total number of transaction and commands + + ProtobufWkt::Value total_transactions; + total_transactions.set_number_value(session_stats_.total_transactions); + fields["total_transactions"] = total_transactions; + + ProtobufWkt::Value transactions_completed; + transactions_completed.set_number_value(session_stats_.transactions_completed); + fields["transactions_completed"] = transactions_completed; + + ProtobufWkt::Value transactions_failed; + transactions_failed.set_number_value(session_stats_.transactions_failed); + fields["transactions_failed"] = transactions_failed; + + ProtobufWkt::Value transactions_aborted; + transactions_aborted.set_number_value(session_stats_.transactions_aborted); + fields["transactions_aborted"] = transactions_aborted; + + ProtobufWkt::Value total_commands; + total_commands.set_number_value(session_stats_.total_commands); + fields["total_commands"] = total_commands; + + ProtobufWkt::Value upstream_session_type; + if (upstream_session_type_ == SmtpUtils::SessionType::PlainText) { + upstream_session_type.set_string_value("PlainText"); + } else { + upstream_session_type.set_string_value("TLS"); + } + fields["upstream_session_type"] = upstream_session_type; +} + +void SmtpSession::setSessionMetadata() { + StreamInfo::StreamInfo& parent_stream_info = callbacks_->getStreamInfo(); + ProtobufWkt::Struct metadata( + (*parent_stream_info.dynamicMetadata() + .mutable_filter_metadata())[NetworkFilterNames::get().SmtpProxy]); + + auto& fields = *metadata.mutable_fields(); + // Emit SMTP session metadata + ProtobufWkt::Struct session_metadata; + encode(session_metadata); + fields["session_metadata"].mutable_struct_value()->CopyFrom(session_metadata); + + ProtobufWkt::Value session_id; + session_id.set_string_value(session_id_); + fields["session_id"] = session_id; + + parent_stream_info.setDynamicMetadata(NetworkFilterNames::get().SmtpProxy, metadata); + // callbacks_->emitLogEntry(parent_stream_info); +} + +void SmtpSession::terminateSession() { + setState(SmtpSession::State::SessionTerminated); + callbacks_->incSmtpSessionsCompleted(); + if (transaction_in_progress_) { + abortTransaction(); + } + setSessionMetadata(); +} + +void SmtpSession::abortTransaction() { + callbacks_->incSmtpTransactionsAborted(); + getTransaction()->setStatus(SmtpUtils::statusAborted); + getSessionStats().transactions_aborted++; + onTransactionComplete(); +} + +void SmtpSession::onTransactionComplete() { + + callbacks_->incSmtpTransactions(); + session_stats_.total_transactions++; + transaction_in_progress_ = false; + getTransaction()->onComplete(); + getTransaction()->emitLog(); + endTransaction(); +} + +void SmtpSession::endTransaction() { + if (smtp_transaction_ != nullptr) { + delete smtp_transaction_; + smtp_transaction_ = nullptr; + } +} + +void SmtpSession::handleDownstreamTls() { + setState(SmtpSession::State::DownstreamTlsNegotiation); + if (!callbacks_->downstreamStartTls( + SmtpUtils::generateResponse(220, {2, 0, 0}, "Ready to start TLS"))) { + // callback returns false if connection is switched to tls i.e. tls termination is + // successful. + callbacks_->incTlsTerminatedSessions(); + setSessionEncrypted(true); + setState(SmtpSession::State::SessionInProgress); + } else { + // error while switching transport socket to tls. + callbacks_->incTlsTerminationErrors(); + callbacks_->sendReplyDownstream( + SmtpUtils::generateResponse(550, {5, 0, 0}, "TLS Handshake error")); + terminateSession(); + callbacks_->closeDownstreamConnection(); + } +} + +} // namespace SmtpProxy +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy diff --git a/contrib/smtp_proxy/filters/network/source/smtp_session.h b/contrib/smtp_proxy/filters/network/source/smtp_session.h new file mode 100644 index 000000000000..ffd8edbf649d --- /dev/null +++ b/contrib/smtp_proxy/filters/network/source/smtp_session.h @@ -0,0 +1,138 @@ +#pragma once +#include + +#include "source/common/common/logger.h" + +#include "contrib/smtp_proxy/filters/network/source/smtp_command.h" +#include "contrib/smtp_proxy/filters/network/source/smtp_decoder.h" +#include "contrib/smtp_proxy/filters/network/source/smtp_handler.h" +#include "contrib/smtp_proxy/filters/network/source/smtp_transaction.h" + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace SmtpProxy { + +class SmtpSession : public SmtpHandler { +public: + enum class State { + ConnectionRequest = 0, + ConnectionSuccess = 1, + SessionInitRequest = 2, + SessionInProgress = 3, + SessionTerminationRequest = 4, + SessionTerminated = 5, + UpstreamTlsNegotiation = 6, + DownstreamTlsNegotiation = 7, + SessionAuthRequest = 8, + SessionResetRequest = 9, + XReqIdTransfer = 10, + }; + + struct SmtpSessionStats { + int transactions_failed; + int transactions_completed; + int transactions_aborted; + int total_transactions; + int total_commands; + }; + + SmtpSession(DecoderCallbacks* callbacks, TimeSource& time_source, + Random::RandomGenerator& random_generator); + + ~SmtpSession() { + delete smtp_transaction_; + smtp_transaction_ = nullptr; + } + + void setState(SmtpSession::State state) { state_ = state; } + SmtpSession::State getState() { return state_; } + + SmtpTransaction* getTransaction() { return smtp_transaction_; } + void createNewTransaction(); + void endTransaction(); + + void setTransactionState(SmtpTransaction::State state) { smtp_transaction_->setState(state); }; + SmtpTransaction::State getTransactionState() { return smtp_transaction_->getState(); } + + SmtpSession::SmtpSessionStats& getSessionStats() { return session_stats_; } + + void setSessionEncrypted(bool flag) { session_encrypted_ = flag; } + bool isSessionEncrypted() const { return session_encrypted_; } + + void encode(ProtobufWkt::Struct& metadata); + + SmtpUtils::Result handleCommand(std::string& command, std::string& args) override; + + SmtpUtils::Result handleEhlo(std::string& command); + SmtpUtils::Result handleMail(std::string& args); + SmtpUtils::Result handleRcpt(std::string& args); + SmtpUtils::Result handleData(std::string& args); + SmtpUtils::Result handleReset(std::string& args); + SmtpUtils::Result handleQuit(std::string& args); + SmtpUtils::Result handleAuth(); + SmtpUtils::Result handleStarttls(); + SmtpUtils::Result handleOtherCmds(std::string& args); + + SmtpUtils::Result handleResponse(uint16_t& response_code, std::string& response) override; + SmtpUtils::Result handleConnResponse(uint16_t& response_code, std::string& response); + SmtpUtils::Result handleEhloResponse(uint16_t& response_code, std::string& response); + SmtpUtils::Result handleMailResponse(uint16_t& response_code, std::string& response); + SmtpUtils::Result handleRcptResponse(uint16_t& response_code, std::string& response); + SmtpUtils::Result handleDataResponse(uint16_t& response_code, std::string& response); + SmtpUtils::Result handleResetResponse(uint16_t& response_code, std::string& response); + SmtpUtils::Result handleQuitResponse(uint16_t& response_code, std::string& response); + SmtpUtils::Result handleAuthResponse(uint16_t& response_code, std::string& response); + SmtpUtils::Result handleStarttlsResponse(uint16_t& response_code, std::string& response); + SmtpUtils::Result handleXReqIdResponse(uint16_t& response_code, std::string& response); + SmtpUtils::Result handleOtherResponse(uint16_t& response_code, std::string& response); + + void abortTransaction(); + void handleDownstreamTls(); + + void newCommand(const std::string& name, SmtpCommand::Type type); + SmtpUtils::Result storeResponse(std::string response, uint16_t response_code); + std::string& getResponseOnHold() { return response_on_hold_; } + void setResponseOnHold(std::string& resp) { response_on_hold_ = resp; } + bool isDataTransferInProgress() override { return data_transfer_in_progress_; } + bool isTerminated() override { return state_ == State::SessionTerminated; } + void terminateSession(); + void setDataTransferInProgress(bool status) { data_transfer_in_progress_ = status; } + bool isCommandInProgress() override { return command_in_progress_; } + + bool isAuthenticated() { return auth_complete_; } + void setAuthStatus(bool status) { auth_complete_ = status; } + + std::shared_ptr getCurrentCommand() { return current_command_; } + void updateBytesMeterOnCommand(Buffer::Instance& data) override; + void updateBytesMeterOnResponse(Buffer::Instance& data); + + void setSessionMetadata(); + void onTransactionComplete(); + + bool isXReqIdSent() { return x_req_id_sent_; } + +private: + std::string session_id_; + SmtpSession::State state_{State::ConnectionRequest}; + SmtpTransaction* smtp_transaction_{}; + SmtpSession::SmtpSessionStats session_stats_ = {}; + bool session_encrypted_{false}; // tells if smtp session is encrypted + DecoderCallbacks* callbacks_{}; + TimeSource& time_source_; + Random::RandomGenerator& random_generator_; + std::shared_ptr current_command_; + std::vector> session_commands_; + std::string response_on_hold_; + bool data_transfer_in_progress_{false}; + bool transaction_in_progress_{false}; + bool command_in_progress_{false}; + bool auth_complete_{false}; + bool x_req_id_sent_{false}; + SmtpUtils::SessionType upstream_session_type_{SmtpUtils::SessionType::PlainText}; +}; + +} // namespace SmtpProxy +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy diff --git a/contrib/smtp_proxy/filters/network/source/smtp_transaction.cc b/contrib/smtp_proxy/filters/network/source/smtp_transaction.cc new file mode 100644 index 000000000000..28431e897ac7 --- /dev/null +++ b/contrib/smtp_proxy/filters/network/source/smtp_transaction.cc @@ -0,0 +1,99 @@ +#include "contrib/smtp_proxy/filters/network/source/smtp_transaction.h" + +#include "source/common/common/logger.h" +#include "source/extensions/filters/network/well_known_names.h" + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace SmtpProxy { + +SmtpTransaction::SmtpTransaction(std::string& session_id, DecoderCallbacks* callbacks, + TimeSource& time_source, Random::RandomGenerator& random_generator) + : session_id_(session_id), callbacks_(callbacks), time_source_(time_source), + random_generator_(random_generator), + stream_info_(time_source_, callbacks_->connection().connectionInfoProviderSharedPtr()) { + + StreamInfo::StreamInfo& parent_stream_info = callbacks_->getStreamInfo(); + + stream_info_.setUpstreamInfo(parent_stream_info.upstreamInfo()); + stream_info_.setDownstreamBytesMeter(std::make_shared()); + + stream_info_.setStreamIdProvider( + std::make_shared(random_generator_.uuid())); + + transaction_id_ = stream_info_.getStreamIdProvider()->toStringView().value(); +} + +void SmtpTransaction::onComplete() { stream_info_.onRequestComplete(); } + +void SmtpTransaction::emitLog() { + // Emit per transaction log + + ProtobufWkt::Struct metadata( + (*stream_info_.dynamicMetadata() + .mutable_filter_metadata())[NetworkFilterNames::get().SmtpProxy]); + + auto& fields = *metadata.mutable_fields(); + fields["transaction_metadata"].mutable_struct_value()->Clear(); + + ProtobufWkt::Struct transaction_metadata; + encode(transaction_metadata); + fields["transaction_metadata"].mutable_struct_value()->CopyFrom(transaction_metadata); + + ProtobufWkt::Value session_id; + session_id.set_string_value(session_id_); + fields["session_id"] = session_id; + + stream_info_.setDynamicMetadata(NetworkFilterNames::get().SmtpProxy, metadata); + callbacks_->emitLogEntry(stream_info_); +} + +void SmtpTransaction::encode(ProtobufWkt::Struct& metadata) { + + auto& fields = *(metadata.mutable_fields()); + + ProtobufWkt::Value status; + status.set_string_value(getStatus()); + fields["status"] = status; + + ProtobufWkt::Value sender; + sender.set_string_value(sender_); + fields["sender"] = sender; + + ProtobufWkt::Value payload; + payload.set_number_value(payload_size_); + fields["payload_size"] = payload; + + ProtobufWkt::ListValue recipients; + for (auto& rcpt : recipients_) { + recipients.add_values()->set_string_value(rcpt); + } + fields["recipients"].mutable_list_value()->CopyFrom(recipients); + + ProtobufWkt::ListValue commands; + for (auto command : trxn_commands_) { + ProtobufWkt::Struct data_struct; + auto& fields = *(data_struct.mutable_fields()); + + ProtobufWkt::Value name; + name.set_string_value(command->getName()); + fields["command_name"] = name; + + ProtobufWkt::Value response_code; + response_code.set_number_value(command->getResponseCode()); + fields["response_code"] = response_code; + + ProtobufWkt::Value duration; + duration.set_number_value(command->getDuration()); + fields["duration"] = duration; + + commands.add_values()->mutable_struct_value()->CopyFrom(data_struct); + } + fields["commands"].mutable_list_value()->CopyFrom(commands); +} + +} // namespace SmtpProxy +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy diff --git a/contrib/smtp_proxy/filters/network/source/smtp_transaction.h b/contrib/smtp_proxy/filters/network/source/smtp_transaction.h new file mode 100644 index 000000000000..f38a34013e44 --- /dev/null +++ b/contrib/smtp_proxy/filters/network/source/smtp_transaction.h @@ -0,0 +1,84 @@ +#pragma once + +#include +#include + +#include "envoy/stream_info/stream_info.h" + +#include "source/common/common/logger.h" +#include "source/common/protobuf/utility.h" +#include "source/common/stream_info/stream_info_impl.h" + +#include "contrib/smtp_proxy/filters/network/source/smtp_command.h" +#include "contrib/smtp_proxy/filters/network/source/smtp_decoder.h" +#include "contrib/smtp_proxy/filters/network/source/smtp_utils.h" + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace SmtpProxy { + +// Class stores data about the current state of a transaction between SMTP client and server. + +class SmtpTransaction { +public: + enum class State { + None = 0, + TransactionRequest = 1, + TransactionInProgress = 2, + TransactionAbortRequest = 3, + TransactionAborted = 4, + MailDataTransferRequest = 5, + RcptCommand = 6, + TransactionCompleted = 7, + XReqIdTransfer = 8, + }; + + SmtpTransaction(std::string& session_id, DecoderCallbacks* callbacks, TimeSource& time_source, + Random::RandomGenerator& random_generator); + + std::string& getTransactionId() { return transaction_id_; } + void setState(SmtpTransaction::State state) { state_ = state; } + SmtpTransaction::State getState() { return state_; } + + void setStatus(const std::string status) { status_ = status; } + const std::string& getStatus() const { return status_; } + + void setSender(std::string& sender) { sender_ = sender; } + std::string& getSender() { return sender_; } + + void addRcpt(std::string& rcpt) { recipients_.push_back(rcpt); } + + uint8_t getNoOfRecipients() { return recipients_.size(); } + // Adds number of bytes to mail data payload. + void addPayloadBytes(uint64_t bytes) { payload_size_ += bytes; } + void encode(ProtobufWkt::Struct& metadata); + + void addTrxnCommand(std::shared_ptr command) { trxn_commands_.push_back(command); } + void onComplete(); + void emitLog(); + void setXReqIdSent(bool status) { x_req_id_sent_ = status; } + bool isXReqIdSent() { return x_req_id_sent_; } + StreamInfo::StreamInfo& getStreamInfo() { return stream_info_; } + +private: + std::string transaction_id_; + std::string session_id_; + SmtpTransaction::State state_{State::None}; + // Transaction status + std::string status_; + std::string sender_; + std::vector recipients_; + uint64_t payload_size_ = 0; + std::vector> trxn_commands_; + DecoderCallbacks* callbacks_{}; + TimeSource& time_source_; + Random::RandomGenerator& random_generator_; + StreamInfo::StreamInfoImpl stream_info_; + bool x_req_id_sent_{false}; +}; + +} // namespace SmtpProxy +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy diff --git a/contrib/smtp_proxy/filters/network/source/smtp_utils.cc b/contrib/smtp_proxy/filters/network/source/smtp_utils.cc new file mode 100644 index 000000000000..78847dc8cf20 --- /dev/null +++ b/contrib/smtp_proxy/filters/network/source/smtp_utils.cc @@ -0,0 +1,60 @@ +#include "contrib/smtp_proxy/filters/network/source/smtp_utils.h" + +#include + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace SmtpProxy { + +// Function to generate SMTP response with enhanced status code +std::string SmtpUtils::generateResponse(int code, EnhancedCode enhCode, std::string text) { + std::ostringstream response; + + // All responses must include an enhanced code, if it is missing - use + // a generic code X.0.0. + if (enhCode == EnhancedCodeNotSet) { + int cat = code / 100; + switch (cat) { + case 2: + case 4: + case 5: + enhCode = {cat, 0, 0}; + break; + default: + enhCode = EnhancedCodeNotSet; + break; + } + } + + // for (auto it = text.begin(); it != text.end() - 1; ++it) { + // response << code << "- " << *it << "\r\n"; + // } + + if (enhCode == EnhancedCodeNotSet) { + response << code << " " << text << "\r\n"; + } else { + response << code << " " << enhCode[0] << "." << enhCode[1] << "." << enhCode[2] << " " << text + << "\r\n"; + } + + return response.str(); +} + +// Extract the email address between the < and > characters. + +std::string SmtpUtils::extractAddress(std::string& arg) { + std::string address = ""; + size_t start_pos = arg.find('<'); + size_t end_pos = arg.find('>'); + + if ((start_pos != std::string::npos && end_pos != std::string::npos) && (start_pos < end_pos)) { + address = arg.substr(start_pos + 1, end_pos - start_pos - 1); + } + return address; +} + +} // namespace SmtpProxy +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy diff --git a/contrib/smtp_proxy/filters/network/source/smtp_utils.h b/contrib/smtp_proxy/filters/network/source/smtp_utils.h new file mode 100644 index 000000000000..3074280c560a --- /dev/null +++ b/contrib/smtp_proxy/filters/network/source/smtp_utils.h @@ -0,0 +1,56 @@ +#pragma once +#include + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace SmtpProxy { + +class SmtpUtils { +public: + typedef std::array EnhancedCode; + static constexpr EnhancedCode EnhancedCodeNotSet = {0, 0, 0}; + // The following values are returned by the decoder, when filter + // passes bytes of data via onData/onWrite method: + enum class Result { + ReadyForNext, // Decoder processed previous message and is ready for the next message. + Stopped, // Received and processed message disrupts the current flow. Decoder stopped accepting + // data. This happens when decoder wants filter to perform some action, for example to + // call starttls transport socket to enable TLS. + ResumeLastResponse + }; + + enum class SessionType { PlainText, Tls }; + + inline static const char* smtpCrlfSuffix = "\r\n"; + inline static const char* smtpHeloCommand = "HELO"; + inline static const char* smtpEhloCommand = "EHLO"; + inline static const char* smtpAuthCommand = "AUTH"; + inline static const char* smtpMailCommand = "MAIL"; + inline static const char* smtpRcptCommand = "RCPT"; + inline static const char* smtpDataCommand = "DATA"; + inline static const char* smtpQuitCommand = "QUIT"; + inline static const char* smtpRsetCommand = "RSET"; + inline static const char* startTlsCommand = "STARTTLS"; + inline static const char* xReqIdCommand = "X-REQUEST-ID"; + inline static const char* ehloFirstMsg = "Please introduce yourself first"; + inline static const char* syntaxErrorNoParamsAllowed = + "501 Syntax error (no parameters allowed)\r\n"; + inline static const char* outOfOrderCommandResponse = "503 Bad sequence of commands\r\n"; + inline static const char* readyToStartTlsResponse = "220 2.0.0 Ready to start TLS\r\n"; + inline static const char* tlsHandshakeErrorResponse = "550 5.0.0 TLS Handshake error\r\n"; + inline static const char* tlsNotSupportedResponse = "502 TLS not supported\r\n"; + inline static const char* mailboxUnavailableResponse = + "450 Requested mail action not taken: mailbox unavailable\r\n"; + + inline static const char* statusSuccess = "Success"; + inline static const char* statusFailed = "Failed"; + inline static const char* statusAborted = "Aborted"; + static std::string generateResponse(int code, EnhancedCode enhCode, std::string text); + static std::string extractAddress(std::string& arg); +}; + +} // namespace SmtpProxy +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy diff --git a/contrib/smtp_proxy/filters/network/test/BUILD b/contrib/smtp_proxy/filters/network/test/BUILD new file mode 100644 index 000000000000..e2d2473d92a3 --- /dev/null +++ b/contrib/smtp_proxy/filters/network/test/BUILD @@ -0,0 +1,64 @@ +load( + "//bazel:envoy_build_system.bzl", + "envoy_cc_test", + "envoy_cc_test_library", + "envoy_contrib_package", +) + +licenses(["notice"]) # Apache 2 + +envoy_contrib_package() + +envoy_cc_test( + name = "smtp_decoder_tests", + srcs = [ + "smtp_decoder_test.cc", + ], + deps = [ + ":smtp_test_utils", + "//contrib/smtp_proxy/filters/network/source:filter", + "//test/mocks:common_lib", + "//test/mocks/network:network_mocks", + ], +) + +envoy_cc_test( + name = "smtp_session_tests", + srcs = [ + "smtp_session_test.cc", + ], + deps = [ + ":smtp_test_utils", + "//contrib/smtp_proxy/filters/network/source:smtp_session_lib", + "//test/mocks:common_lib", + "//test/mocks/network:network_mocks", + ], +) + +envoy_cc_test( + name = "smtp_filter_tests", + srcs = [ + "smtp_filter_test.cc", + ], + deps = [ + ":smtp_test_utils", + "//contrib/smtp_proxy/filters/network/source:config", + "//contrib/smtp_proxy/filters/network/source:filter", + "//test/mocks:common_lib", + "//test/mocks/buffer:buffer_mocks", + "//test/mocks/network:network_mocks", + "//test/mocks/stream_info:stream_info_mocks", + ], +) + +envoy_cc_test_library( + name = "smtp_test_utils", + hdrs = ["smtp_test_utils.h"], + deps = [ + "//contrib/smtp_proxy/filters/network/source:smtp_decoder_lib", + "//test/mocks:common_lib", + "//test/mocks/buffer:buffer_mocks", + "//test/mocks/network:network_mocks", + "//test/mocks/stream_info:stream_info_mocks", + ], +) diff --git a/contrib/smtp_proxy/filters/network/test/smtp_decoder_test.cc b/contrib/smtp_proxy/filters/network/test/smtp_decoder_test.cc new file mode 100644 index 000000000000..1f5b1085f3bc --- /dev/null +++ b/contrib/smtp_proxy/filters/network/test/smtp_decoder_test.cc @@ -0,0 +1,296 @@ +#include +#include + +#include "test/mocks/common.h" +#include "test/mocks/network/mocks.h" + +#include "contrib/smtp_proxy/filters/network/source/smtp_decoder_impl.h" +#include "contrib/smtp_proxy/filters/network/source/smtp_utils.h" +#include "contrib/smtp_proxy/filters/network/test/smtp_test_utils.h" + +using testing::_; +using testing::Eq; +using testing::Invoke; +using testing::NiceMock; +using testing::Ref; +using testing::Return; +using testing::ReturnRef; + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace SmtpProxy { + +class MockSmtpSession : public SmtpSession { +public: + MockSmtpSession(DecoderCallbacks* callbacks, TimeSource& time_source, + Random::RandomGenerator& random_generator) + : SmtpSession(callbacks, time_source, random_generator) {} + ~MockSmtpSession() {} + MOCK_METHOD(bool, isTerminated, (), (override)); + MOCK_METHOD(bool, isDataTransferInProgress, (), (override)); + MOCK_METHOD(bool, isCommandInProgress, (), (override)); + // MOCK_METHOD(SmtpSession::State, getState, (), (override)); + MOCK_METHOD(void, updateBytesMeterOnCommand, (Buffer::Instance & data), (override)); + MOCK_METHOD(SmtpUtils::Result, handleCommand, (std::string & command, std::string& args), + (override)); + MOCK_METHOD(SmtpUtils::Result, handleResponse, (uint16_t & response_code, std::string& response), + (override)); +}; + +class DecoderImplTest : public ::testing::Test { +public: + void SetUp() override { + data_ = std::make_unique(); + session_ = std::make_unique>(&callbacks_, time_source_, random_); + decoder_ = std::make_unique(&callbacks_, time_source_, random_); + decoder_->setSession(session_.get()); + } + +protected: + std::unique_ptr data_; + NiceMock callbacks_; + std::unique_ptr> session_; + std::unique_ptr decoder_; + NiceMock random_; + NiceMock time_source_; +}; + +TEST_F(DecoderImplTest, TestParseCommand) { + // When session is terminated + data_->add("EHLO test.com\r\n"); + EXPECT_CALL(*session_, isTerminated()).WillOnce(Return(true)); + EXPECT_EQ(SmtpUtils::Result::ReadyForNext, decoder_->parseCommand(*data_)); + + testing::Mock::VerifyAndClearExpectations(session_.get()); + + EXPECT_CALL(*session_, isTerminated()).WillOnce(Return(false)); + EXPECT_CALL(*session_, isDataTransferInProgress()).WillOnce(Return(true)); + EXPECT_CALL(*session_, updateBytesMeterOnCommand(Ref(*data_))); + + EXPECT_EQ(SmtpUtils::Result::ReadyForNext, decoder_->parseCommand(*data_)); + + // SMTP command without CRLF ending + testing::Mock::VerifyAndClearExpectations(session_.get()); + data_->drain(data_->length()); + data_->add("EHLO test.com"); + + EXPECT_CALL(*session_, isTerminated()).WillOnce(Return(false)); + EXPECT_CALL(*session_, isDataTransferInProgress()).WillOnce(Return(false)); + + EXPECT_EQ(SmtpUtils::Result::ReadyForNext, decoder_->parseCommand(*data_)); + + // SMTP command with length < 4 (excluding CRLF) + testing::Mock::VerifyAndClearExpectations(session_.get()); + data_->drain(data_->length()); + data_->add("EHL\r\n"); + + EXPECT_CALL(*session_, isTerminated()).WillOnce(Return(false)); + EXPECT_CALL(*session_, isDataTransferInProgress()).WillOnce(Return(false)); + + EXPECT_EQ(SmtpUtils::Result::ReadyForNext, decoder_->parseCommand(*data_)); + + // SMTP command with length = 4 (excluding CRLF) + testing::Mock::VerifyAndClearExpectations(session_.get()); + data_->drain(data_->length()); + data_->add("RSET\r\n"); + + // Expected command and args parsed. + std::string command = "RSET"; + std::string args = ""; + EXPECT_CALL(*session_, isTerminated()).WillOnce(Return(false)); + EXPECT_CALL(*session_, isDataTransferInProgress()).WillOnce(Return(false)); + EXPECT_CALL(*session_, handleCommand(command, args)) + .WillOnce(Return(SmtpUtils::Result::ReadyForNext)); + + EXPECT_EQ(SmtpUtils::Result::ReadyForNext, decoder_->parseCommand(*data_)); + + // SMTP command with length > 4 (excluding CRLF), below cmd will not be processed and it will be + // forwarded to upstream. + testing::Mock::VerifyAndClearExpectations(session_.get()); + data_->drain(data_->length()); + data_->add("RSETY\r\n"); + + command = ""; + args = ""; + EXPECT_CALL(*session_, isTerminated()).WillOnce(Return(false)); + EXPECT_CALL(*session_, isDataTransferInProgress()).WillOnce(Return(false)); + EXPECT_CALL(*session_, handleCommand(command, args)) + .WillOnce(Return(SmtpUtils::Result::ReadyForNext)); + + EXPECT_EQ(SmtpUtils::Result::ReadyForNext, decoder_->parseCommand(*data_)); + + // SMTP command with length = 6 (excluding CRLF) + testing::Mock::VerifyAndClearExpectations(session_.get()); + data_->drain(data_->length()); + data_->add("RSETYZ\r\n"); + + command = ""; + args = ""; + EXPECT_CALL(*session_, isTerminated()).WillOnce(Return(false)); + EXPECT_CALL(*session_, isDataTransferInProgress()).WillOnce(Return(false)); + EXPECT_CALL(*session_, handleCommand(command, args)) + .WillOnce(Return(SmtpUtils::Result::ReadyForNext)); + + EXPECT_EQ(SmtpUtils::Result::ReadyForNext, decoder_->parseCommand(*data_)); + + // SMTP command with length = 4 followed by a space and no args + testing::Mock::VerifyAndClearExpectations(session_.get()); + data_->drain(data_->length()); + data_->add("RSET \r\n"); + + // Expected command and args parsed. + command = ""; + args = ""; + EXPECT_CALL(*session_, isTerminated()).WillOnce(Return(false)); + EXPECT_CALL(*session_, isDataTransferInProgress()).WillOnce(Return(false)); + EXPECT_CALL(*session_, handleCommand(command, args)) + .WillOnce(Return(SmtpUtils::Result::ReadyForNext)); + + EXPECT_EQ(SmtpUtils::Result::ReadyForNext, decoder_->parseCommand(*data_)); + + // SMTP command with length = 4 followed by a 2 spaces + testing::Mock::VerifyAndClearExpectations(session_.get()); + data_->drain(data_->length()); + data_->add("RSET \r\n"); + + // Expected command and args parsed. + command = ""; + args = ""; + EXPECT_CALL(*session_, isTerminated()).WillOnce(Return(false)); + EXPECT_CALL(*session_, isDataTransferInProgress()).WillOnce(Return(false)); + EXPECT_CALL(*session_, handleCommand(command, args)) + .WillOnce(Return(SmtpUtils::Result::ReadyForNext)); + + EXPECT_EQ(SmtpUtils::Result::ReadyForNext, decoder_->parseCommand(*data_)); + + // SMTP command with length = 4 followed by a space and arg with 1 char + testing::Mock::VerifyAndClearExpectations(session_.get()); + data_->drain(data_->length()); + data_->add("CMMD Y\r\n"); + + // Expected command and args parsed. + command = "CMMD"; + args = "Y"; + EXPECT_CALL(*session_, isTerminated()).WillOnce(Return(false)); + EXPECT_CALL(*session_, isDataTransferInProgress()).WillOnce(Return(false)); + EXPECT_CALL(*session_, handleCommand(command, args)) + .WillOnce(Return(SmtpUtils::Result::ReadyForNext)); + + EXPECT_EQ(SmtpUtils::Result::ReadyForNext, decoder_->parseCommand(*data_)); + + // A valid SMTP command with args + testing::Mock::VerifyAndClearExpectations(session_.get()); + data_->drain(data_->length()); + data_->add("MAIL FROM:\r\n"); + + // Expected command and args parsed. + command = "MAIL"; + args = "FROM:"; + EXPECT_CALL(*session_, isTerminated()).WillOnce(Return(false)); + EXPECT_CALL(*session_, isDataTransferInProgress()).WillOnce(Return(false)); + EXPECT_CALL(*session_, handleCommand(command, args)) + .WillOnce(Return(SmtpUtils::Result::ReadyForNext)); + + EXPECT_EQ(SmtpUtils::Result::ReadyForNext, decoder_->parseCommand(*data_)); + + // SMTP starttls command + testing::Mock::VerifyAndClearExpectations(session_.get()); + data_->drain(data_->length()); + data_->add("STARTTLS\r\n"); + + command = "STARTTLS"; + args = ""; + EXPECT_CALL(*session_, isTerminated()).WillOnce(Return(false)); + EXPECT_CALL(*session_, isDataTransferInProgress()).WillOnce(Return(false)); + EXPECT_CALL(*session_, handleCommand(command, args)) + .WillOnce(Return(SmtpUtils::Result::ReadyForNext)); + + EXPECT_EQ(SmtpUtils::Result::ReadyForNext, decoder_->parseCommand(*data_)); + + // SMTP starttls command followed by a char/space, will not be parsed + testing::Mock::VerifyAndClearExpectations(session_.get()); + data_->drain(data_->length()); + data_->add("STARTTLS \r\n"); + + command = ""; + args = ""; + EXPECT_CALL(*session_, isTerminated()).WillOnce(Return(false)); + EXPECT_CALL(*session_, isDataTransferInProgress()).WillOnce(Return(false)); + EXPECT_CALL(*session_, handleCommand(command, args)) + .WillOnce(Return(SmtpUtils::Result::ReadyForNext)); + + EXPECT_EQ(SmtpUtils::Result::ReadyForNext, decoder_->parseCommand(*data_)); +} + +TEST_F(DecoderImplTest, TestParseResponse) { + + session_->setState(SmtpSession::State::ConnectionSuccess); + + // No command is currently being processed,so response will not be processed + EXPECT_CALL(*session_, isCommandInProgress()).WillOnce(Return(false)); + data_->add("220 OK"); + EXPECT_EQ(SmtpUtils::Result::ReadyForNext, decoder_->parseResponse(*data_)); + testing::Mock::VerifyAndClearExpectations(session_.get()); + + // SMTP response without CRLF ending, will not be processed. + data_->drain(data_->length()); + data_->add("220 OK"); + EXPECT_CALL(*session_, isCommandInProgress()).WillOnce(Return(true)); + EXPECT_EQ(SmtpUtils::Result::ReadyForNext, decoder_->parseResponse(*data_)); + testing::Mock::VerifyAndClearExpectations(session_.get()); + + // SMTP response length < 3 (exlcuding CRLF) + data_->drain(data_->length()); + data_->add("22\r\n"); + EXPECT_CALL(*session_, isCommandInProgress()).WillOnce(Return(true)); + EXPECT_EQ(SmtpUtils::Result::ReadyForNext, decoder_->parseResponse(*data_)); + testing::Mock::VerifyAndClearExpectations(session_.get()); + + // SMTP response length = 3 (exlcuding CRLF) + data_->drain(data_->length()); + data_->add("220\r\n"); + + // Expected response code and response string + uint16_t resp_code = 220; + std::string resp = "220"; + EXPECT_CALL(*session_, isCommandInProgress()).WillOnce(Return(true)); + EXPECT_CALL(*session_, handleResponse(resp_code, resp)) + .WillOnce(Return(SmtpUtils::Result::ReadyForNext)); + EXPECT_EQ(SmtpUtils::Result::ReadyForNext, decoder_->parseResponse(*data_)); + testing::Mock::VerifyAndClearExpectations(session_.get()); + + // SMTP response received, No command in progress but session state is ConnectionRequest + data_->drain(data_->length()); + data_->add("220 OK\r\n"); + EXPECT_CALL(*session_, isCommandInProgress()).WillOnce(Return(false)); + session_->setState(SmtpSession::State::ConnectionRequest); + + // Expected response code and response string + resp_code = 220; + resp = "220 OK"; + EXPECT_CALL(*session_, handleResponse(resp_code, resp)) + .WillOnce(Return(SmtpUtils::Result::ReadyForNext)); + EXPECT_EQ(SmtpUtils::Result::ReadyForNext, decoder_->parseResponse(*data_)); + testing::Mock::VerifyAndClearExpectations(session_.get()); + + session_->setState(SmtpSession::State::ConnectionSuccess); + + // SMTP response with invalid response code + data_->drain(data_->length()); + data_->add("abc OK\r\n"); + + // Expected response code and response string + resp_code = 0; + resp = "abc OK"; + EXPECT_CALL(*session_, isCommandInProgress()).WillOnce(Return(true)); + EXPECT_CALL(*session_, handleResponse(resp_code, resp)) + .WillOnce(Return(SmtpUtils::Result::ReadyForNext)); + EXPECT_EQ(SmtpUtils::Result::ReadyForNext, decoder_->parseResponse(*data_)); + testing::Mock::VerifyAndClearExpectations(session_.get()); +} + +} // namespace SmtpProxy +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy \ No newline at end of file diff --git a/contrib/smtp_proxy/filters/network/test/smtp_filter_test.cc b/contrib/smtp_proxy/filters/network/test/smtp_filter_test.cc new file mode 100644 index 000000000000..0d05bd5cd661 --- /dev/null +++ b/contrib/smtp_proxy/filters/network/test/smtp_filter_test.cc @@ -0,0 +1,180 @@ +#include + +#include "source/common/buffer/buffer_impl.h" + +#include "test/mocks/common.h" +#include "test/mocks/network/mocks.h" + +#include "contrib/smtp_proxy/filters/network/source/smtp_filter.h" +#include "contrib/smtp_proxy/filters/network/source/smtp_utils.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +using testing::NiceMock; + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace SmtpProxy { + +class SmtpFilterTest : public testing::Test { +public: + void SetUp() override { + config_ = std::make_shared(config_options_, scope_); + filter_ = std::make_unique(config_, time_source_, random_); + filter_->initializeReadFilterCallbacks(filter_callbacks_); + } + + SmtpFilterConfigSharedPtr config_; + std::string stat_prefix_{"test."}; + bool tracing_; + const std::vector access_logs_; + + SmtpFilterConfig::SmtpFilterConfigOptions config_options_{ + stat_prefix_, tracing_, + envoy::extensions::filters::network::smtp_proxy::v3alpha::SmtpProxy::DISABLE, access_logs_}; + + std::unique_ptr filter_; + Stats::IsolatedStoreImpl store_; + Stats::Scope& scope_{*store_.rootScope()}; + NiceMock filter_callbacks_; + NiceMock connection_; + Buffer::OwnedImpl data_; + NiceMock time_source_; + NiceMock random_; +}; + +// Test New Session counter increment +TEST_F(SmtpFilterTest, NewSessionStatsTest) { + + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onNewConnection()); + EXPECT_EQ(filter_->getSession()->getState(), SmtpSession::State::ConnectionRequest); + + EXPECT_EQ(1, config_->stats().smtp_session_requests_.value()); +} + +TEST_F(SmtpFilterTest, TestDownstreamStarttls) { + + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onNewConnection()); + // Upstream TLS is disabled, testing only downstream starttls handling + filter_->getConfig()->upstream_tls_ = + envoy::extensions::filters::network::smtp_proxy::v3alpha::SmtpProxy::DISABLE; + ASSERT_FALSE(filter_->upstreamTlsRequired()); + filter_->getSession()->setState(SmtpSession::State::ConnectionSuccess); + + data_.add("EHLO localhost\r\n"); + std::cout << data_.toString() << std::endl; + ASSERT_THAT(Network::FilterStatus::Continue, filter_->onData(data_, false)); + EXPECT_EQ(SmtpSession::State::SessionInitRequest, filter_->getSession()->getState()); + + data_.drain(data_.length()); + + data_.add("250-Hello localhost\r\n250-PIPELINING\r\n250-8BITMIME\r\n250-STARTTLS\r\n"); + ASSERT_THAT(Network::FilterStatus::Continue, filter_->onWrite(data_, false)); + EXPECT_EQ(SmtpSession::State::SessionInProgress, filter_->getSession()->getState()); + + data_.drain(data_.length()); + data_.add("STARTTLS\r\n"); + + // Downstream TLS termination successful after STARTTLS + + EXPECT_CALL(filter_callbacks_, connection()).WillRepeatedly(ReturnRef(connection_)); + Network::Connection::BytesSentCb cb; + EXPECT_CALL(connection_, addBytesSentCallback(_)).WillOnce(testing::SaveArg<0>(&cb)); + Buffer::OwnedImpl buf; + EXPECT_CALL(connection_, write(_, false)).WillOnce(testing::SaveArg<0>(&buf)); + + std::string resp = "220 2.0.0 Ready to start TLS\r\n"; + ASSERT_THAT(Network::FilterStatus::StopIteration, filter_->onData(data_, false)); + ASSERT_STREQ(resp.c_str(), buf.toString().c_str()); + + // Now indicate through the callback that 220 response has been sent. + // Filter should call startSecureTransport and should not close the connection. + EXPECT_CALL(connection_, startSecureTransport()).WillOnce(testing::Return(true)); + EXPECT_CALL(connection_, close(_)).Times(0); + cb(buf.length()); + + EXPECT_EQ(SmtpSession::State::SessionInProgress, filter_->getSession()->getState()); + EXPECT_EQ(config_->stats().smtp_tls_terminated_sessions_.value(), 1); + + // Send starttls command again, receive 503 out of order command response from filter. + buf.drain(buf.length()); + data_.add("STARTTLS\r\n"); + + EXPECT_CALL(connection_, addBytesSentCallback(_)).WillOnce(testing::SaveArg<0>(&cb)); + EXPECT_CALL(connection_, write(_, false)).WillOnce(testing::SaveArg<0>(&buf)); + resp = "502 5.5.1 Already running in TLS\r\n"; + ASSERT_THAT(Network::FilterStatus::StopIteration, filter_->onData(data_, false)); + ASSERT_STREQ(resp.c_str(), buf.toString().c_str()); +} + +TEST_F(SmtpFilterTest, TestSendReplyDownstream) { + // initialize(); + + EXPECT_CALL(filter_callbacks_, connection()).WillRepeatedly(ReturnRef(connection_)); + Network::Connection::BytesSentCb cb; + EXPECT_CALL(connection_, addBytesSentCallback(_)).WillOnce(testing::SaveArg<0>(&cb)); + Buffer::OwnedImpl buf; + EXPECT_CALL(connection_, write(_, false)).WillOnce(testing::SaveArg<0>(&buf)); + + ASSERT_THAT(false, filter_->sendReplyDownstream(SmtpUtils::mailboxUnavailableResponse)); + + ASSERT_STREQ(SmtpUtils::mailboxUnavailableResponse, buf.toString().c_str()); + + filter_callbacks_.connection().close(Network::ConnectionCloseType::NoFlush); + + ASSERT_THAT(true, filter_->sendReplyDownstream(SmtpUtils::mailboxUnavailableResponse)); +} + +TEST_F(SmtpFilterTest, TestUpstreamStartTls) { + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onNewConnection()); + // Upstream TLS is disabled, testing only downstream starttls handling + filter_->getConfig()->upstream_tls_ = + envoy::extensions::filters::network::smtp_proxy::v3alpha::SmtpProxy::REQUIRE; + ASSERT_TRUE(filter_->upstreamTlsRequired()); + filter_->getSession()->setState(SmtpSession::State::ConnectionSuccess); + + data_.add("EHLO localhost\r\n"); + ASSERT_THAT(Network::FilterStatus::Continue, filter_->onData(data_, false)); + EXPECT_EQ(SmtpSession::State::SessionInitRequest, filter_->getSession()->getState()); + + data_.drain(data_.length()); + + data_.add("250-Hello localhost\r\n250-PIPELINING\r\n250-8BITMIME\r\n250-STARTTLS\r\n"); + ASSERT_THAT(Network::FilterStatus::Continue, filter_->onWrite(data_, false)); + EXPECT_EQ(SmtpSession::State::SessionInProgress, filter_->getSession()->getState()); + data_.drain(data_.length()); + data_.add("STARTTLS\r\n"); + + // Upstream TLS termination successful after STARTTLS + + ASSERT_THAT(Network::FilterStatus::Continue, filter_->onData(data_, false)); + ASSERT_EQ(SmtpSession::State::UpstreamTlsNegotiation, filter_->getSession()->getState()); + + data_.drain(data_.length()); + data_.add("220 Ready to start TLS\r\n"); + + EXPECT_CALL(filter_callbacks_, startUpstreamSecureTransport()).WillOnce(testing::Return(true)); + + ASSERT_THAT(Network::FilterStatus::StopIteration, filter_->onWrite(data_, false)); + + EXPECT_CALL(connection_, close(_)).Times(0); + EXPECT_EQ(config_->stats().sessions_upstream_tls_success_.value(), 1); + + filter_->getSession()->setSessionEncrypted(false); + filter_->getSession()->setState(SmtpSession::State::UpstreamTlsNegotiation); + + data_.drain(data_.length()); + data_.add("220 Ready to start TLS\r\n"); + + EXPECT_CALL(filter_callbacks_, startUpstreamSecureTransport()).WillOnce(testing::Return(false)); + + ASSERT_THAT(Network::FilterStatus::StopIteration, filter_->onWrite(data_, false)); + ASSERT_EQ(SmtpSession::State::SessionTerminated, filter_->getSession()->getState()); + EXPECT_EQ(config_->stats().sessions_upstream_tls_failed_.value(), 1); +} + +} // namespace SmtpProxy +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy \ No newline at end of file diff --git a/contrib/smtp_proxy/filters/network/test/smtp_session_test.cc b/contrib/smtp_proxy/filters/network/test/smtp_session_test.cc new file mode 100644 index 000000000000..05800d788f90 --- /dev/null +++ b/contrib/smtp_proxy/filters/network/test/smtp_session_test.cc @@ -0,0 +1,457 @@ +#include +#include + +#include "test/mocks/buffer/mocks.h" +#include "test/mocks/common.h" +#include "test/mocks/network/mocks.h" +#include "test/mocks/stream_info/mocks.h" + +#include "contrib/smtp_proxy/filters/network/source/smtp_session.h" +#include "contrib/smtp_proxy/filters/network/source/smtp_utils.h" +#include "contrib/smtp_proxy/filters/network/test/smtp_test_utils.h" + +using testing::_; +using testing::Eq; +using testing::Invoke; +using testing::NiceMock; +using testing::Ref; +using testing::Return; +using testing::ReturnRef; + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace SmtpProxy { + +class SmtpSessionTest : public ::testing::Test { +public: + void SetUp() override { + data_ = std::make_unique(); + session_ = std::make_unique(&callbacks_, time_source_, random_); + } + +protected: + std::unique_ptr data_; + NiceMock callbacks_; + std::unique_ptr session_; + NiceMock random_; + NiceMock time_source_; + NiceMock mock_connection_; + NiceMock stream_info_; + NiceMock buffer_; +}; + +TEST_F(SmtpSessionTest, TestNewCommand) { + // When session is terminated + std::string cmd = "EHLO"; + SmtpCommand::Type type = SmtpCommand::Type::TransactionCommand; + + session_->newCommand(cmd, type); + EXPECT_TRUE(session_->isCommandInProgress()); + EXPECT_EQ(session_->getCurrentCommand()->getName(), cmd); + EXPECT_EQ(session_->getCurrentCommand()->getType(), type); +} + +TEST_F(SmtpSessionTest, TestHandleCommand) { + std::string cmd = ""; + std::string args = ""; + + EXPECT_EQ(session_->handleCommand(cmd, args), SmtpUtils::Result::ReadyForNext); + EXPECT_FALSE(session_->isCommandInProgress()); + EXPECT_EQ(session_->getCurrentCommand(), nullptr); + + cmd = "EHLO"; + EXPECT_EQ(session_->handleCommand(cmd, args), SmtpUtils::Result::ReadyForNext); + EXPECT_TRUE(session_->isCommandInProgress()); + EXPECT_EQ(session_->getCurrentCommand()->getName(), cmd); + EXPECT_EQ(session_->getState(), SmtpSession::State::SessionInitRequest); +} + +TEST_F(SmtpSessionTest, TestHandleMail) { + + // received MAIL from cmd when session not in progress + std::string arg = "FROM:"; + session_->setState(SmtpSession::State::ConnectionSuccess); + + // ON_CALL(callbacks_, sendReplyDownstream(_)).WillByDefault(testing::Return(false)); + EXPECT_CALL(callbacks_, sendReplyDownstream(SmtpUtils::generateResponse( + 502, {5, 5, 1}, "Please introduce yourself first"))); + + auto result = session_->handleMail(arg); + + EXPECT_EQ(result, SmtpUtils::Result::Stopped); + EXPECT_EQ(session_->getState(), SmtpSession::State::ConnectionSuccess); + testing::Mock::VerifyAndClearExpectations(&callbacks_); + + // invalid FROM arg syntax + arg = "test@test.com"; + session_->setState(SmtpSession::State::SessionInProgress); + // ON_CALL(callbacks_, sendReplyDownstream(_)).WillByDefault(testing::Return(false)); + EXPECT_CALL(callbacks_, sendReplyDownstream(SmtpUtils::generateResponse( + 501, {5, 5, 2}, "Bad MAIL arg syntax of FROM:
"))); + + result = session_->handleMail(arg); + + EXPECT_EQ(result, SmtpUtils::Result::Stopped); + EXPECT_EQ(session_->getState(), SmtpSession::State::SessionInProgress); + testing::Mock::VerifyAndClearExpectations(&callbacks_); + + arg = "FRO:"; + session_->setState(SmtpSession::State::SessionInProgress); + // ON_CALL(callbacks_, sendReplyDownstream(_)).WillByDefault(testing::Return(false)); + EXPECT_CALL(callbacks_, sendReplyDownstream(SmtpUtils::generateResponse( + 501, {5, 5, 2}, "Bad MAIL arg syntax of FROM:
"))); + + result = session_->handleMail(arg); + + EXPECT_EQ(result, SmtpUtils::Result::Stopped); + EXPECT_EQ(session_->getState(), SmtpSession::State::SessionInProgress); + testing::Mock::VerifyAndClearExpectations(&callbacks_); +} + +TEST_F(SmtpSessionTest, TestHandleMail_ValidAddr) { + // Valid FROM address + std::string arg = "FROM:"; + std::string sender = "test@test.com"; + session_->setState(SmtpSession::State::SessionInProgress); + auto result = session_->handleMail(arg); + + EXPECT_EQ(result, SmtpUtils::Result::ReadyForNext); + EXPECT_TRUE(session_->isCommandInProgress()); + EXPECT_EQ(session_->getCurrentCommand()->getName(), SmtpUtils::smtpMailCommand); + EXPECT_EQ(session_->getTransaction()->getSender(), sender); + EXPECT_EQ(session_->getTransactionState(), SmtpTransaction::State::TransactionRequest); +} + +TEST_F(SmtpSessionTest, TestHandleRcpt) { + + // received RCPT TO cmd when transaction is not in progress + std::string arg = "TO:"; + session_->setState(SmtpSession::State::SessionInProgress); + + EXPECT_CALL(callbacks_, sendReplyDownstream(SmtpUtils::generateResponse( + 502, {5, 5, 1}, "Missing MAIL FROM command"))); + + auto result = session_->handleRcpt(arg); + + EXPECT_EQ(result, SmtpUtils::Result::Stopped); + EXPECT_EQ(session_->getState(), SmtpSession::State::SessionInProgress); + EXPECT_EQ(session_->getTransaction(), nullptr); + testing::Mock::VerifyAndClearExpectations(&callbacks_); + + // invalid RCPT TO arg syntax + arg = "test@test.com"; + session_->createNewTransaction(); + session_->setState(SmtpSession::State::SessionInProgress); + EXPECT_CALL(callbacks_, sendReplyDownstream(SmtpUtils::generateResponse( + 501, {5, 5, 2}, "Bad RCPT arg syntax of TO:
"))); + + result = session_->handleRcpt(arg); + + EXPECT_EQ(result, SmtpUtils::Result::Stopped); + EXPECT_EQ(session_->getState(), SmtpSession::State::SessionInProgress); + testing::Mock::VerifyAndClearExpectations(&callbacks_); + + arg = "TO:"; + session_->setState(SmtpSession::State::SessionInProgress); + EXPECT_CALL(callbacks_, sendReplyDownstream(SmtpUtils::generateResponse( + 501, {5, 5, 2}, "Bad RCPT arg syntax of TO:
"))); + + result = session_->handleRcpt(arg); + + EXPECT_EQ(result, SmtpUtils::Result::Stopped); + EXPECT_EQ(session_->getState(), SmtpSession::State::SessionInProgress); + testing::Mock::VerifyAndClearExpectations(&callbacks_); + + // Valid RCPT TO address + arg = "TO:"; + session_->setState(SmtpSession::State::SessionInProgress); + result = session_->handleRcpt(arg); + + EXPECT_EQ(result, SmtpUtils::Result::ReadyForNext); + EXPECT_TRUE(session_->isCommandInProgress()); + EXPECT_EQ(session_->getCurrentCommand()->getName(), SmtpUtils::smtpRcptCommand); + EXPECT_EQ(session_->getTransaction()->getNoOfRecipients(), 1); + EXPECT_EQ(session_->getTransactionState(), SmtpTransaction::State::RcptCommand); +} + +TEST_F(SmtpSessionTest, TestHandleData) { + + // received DATA cmd with arguments + std::string arg = "arg1234"; + session_->setState(SmtpSession::State::SessionInProgress); + + EXPECT_CALL(callbacks_, sendReplyDownstream(SmtpUtils::generateResponse( + 501, {5, 5, 4}, "No params allowed for DATA command"))); + + auto result = session_->handleData(arg); + + EXPECT_EQ(result, SmtpUtils::Result::Stopped); + EXPECT_EQ(session_->getState(), SmtpSession::State::SessionInProgress); + testing::Mock::VerifyAndClearExpectations(&callbacks_); + + // received DATA cmd when no transaction in progress + arg = ""; + ASSERT_EQ(session_->getTransaction(), nullptr); + session_->setState(SmtpSession::State::SessionInProgress); + EXPECT_CALL(callbacks_, sendReplyDownstream(SmtpUtils::generateResponse( + 502, {5, 5, 1}, "Missing RCPT TO command"))); + + result = session_->handleData(arg); + + EXPECT_EQ(result, SmtpUtils::Result::Stopped); + EXPECT_EQ(session_->getState(), SmtpSession::State::SessionInProgress); + ASSERT_EQ(session_->getTransaction(), nullptr); + testing::Mock::VerifyAndClearExpectations(&callbacks_); + + // Received DATA command when no RCTP address is received in a trxn + session_->setState(SmtpSession::State::SessionInProgress); + session_->createNewTransaction(); + arg = ""; + ASSERT_EQ(session_->getTransaction()->getNoOfRecipients(), 0); + EXPECT_CALL(callbacks_, sendReplyDownstream(SmtpUtils::generateResponse( + 502, {5, 5, 1}, "Missing RCPT TO command"))); + + result = session_->handleData(arg); + + EXPECT_EQ(result, SmtpUtils::Result::Stopped); + EXPECT_EQ(session_->getState(), SmtpSession::State::SessionInProgress); + testing::Mock::VerifyAndClearExpectations(&callbacks_); + + // Add rcpt address to trxn. + std::string rcpt = "test@test.com"; + session_->getTransaction()->addRcpt(rcpt); + + arg = ""; + result = session_->handleData(arg); + EXPECT_EQ(result, SmtpUtils::Result::ReadyForNext); + EXPECT_TRUE(session_->isCommandInProgress()); + EXPECT_TRUE(session_->isDataTransferInProgress()); + EXPECT_EQ(session_->getCurrentCommand()->getName(), SmtpUtils::smtpDataCommand); + EXPECT_EQ(session_->getTransactionState(), SmtpTransaction::State::MailDataTransferRequest); +} + +TEST_F(SmtpSessionTest, TestHandleAuth) { + + // received AUTH command when session is not in progress. + session_->setState(SmtpSession::State::ConnectionSuccess); + + EXPECT_CALL(callbacks_, sendReplyDownstream(SmtpUtils::generateResponse( + 502, {5, 5, 1}, "Please introduce yourself first"))); + + auto result = session_->handleAuth(); + + EXPECT_EQ(result, SmtpUtils::Result::Stopped); + EXPECT_EQ(session_->getState(), SmtpSession::State::ConnectionSuccess); + testing::Mock::VerifyAndClearExpectations(&callbacks_); + + // received AUTH cmd when session in progress - valid sequence. + session_->setState(SmtpSession::State::SessionInProgress); + result = session_->handleAuth(); + + EXPECT_EQ(result, SmtpUtils::Result::ReadyForNext); + EXPECT_TRUE(session_->isCommandInProgress()); + EXPECT_EQ(session_->getCurrentCommand()->getName(), SmtpUtils::smtpAuthCommand); + EXPECT_EQ(session_->getState(), SmtpSession::State::SessionAuthRequest); + + // Received AUTH cmd when session is already authenticated. + session_->setAuthStatus(true); + session_->setState(SmtpSession::State::SessionInProgress); + EXPECT_CALL(callbacks_, sendReplyDownstream(SmtpUtils::generateResponse( + 502, {5, 5, 1}, "Already authenticated"))); + + result = session_->handleAuth(); + + EXPECT_EQ(result, SmtpUtils::Result::Stopped); + EXPECT_EQ(session_->getState(), SmtpSession::State::SessionInProgress); +} + +TEST_F(SmtpSessionTest, TestHandleStarttls) { + + // received STARTTLS command and upstream TLS is enabled in config. + EXPECT_CALL(callbacks_, upstreamTlsRequired()).WillOnce(Return(true)); + + auto result = session_->handleStarttls(); + + EXPECT_EQ(result, SmtpUtils::Result::ReadyForNext); + EXPECT_TRUE(session_->isCommandInProgress()); + EXPECT_EQ(session_->getCurrentCommand()->getName(), SmtpUtils::startTlsCommand); + EXPECT_EQ(session_->getState(), SmtpSession::State::UpstreamTlsNegotiation); + testing::Mock::VerifyAndClearExpectations(&callbacks_); + + // received STARTTLS command and upstream TLS is disabled in config i.e. only downstream tls + // termination required. + session_->setState(SmtpSession::State::SessionInProgress); + EXPECT_CALL(callbacks_, upstreamTlsRequired()).WillOnce(Return(false)); + + result = session_->handleStarttls(); + + EXPECT_EQ(result, SmtpUtils::Result::Stopped); + EXPECT_TRUE(session_->isCommandInProgress()); + EXPECT_EQ(session_->getCurrentCommand()->getName(), SmtpUtils::startTlsCommand); + EXPECT_EQ(session_->getState(), SmtpSession::State::SessionInProgress); + + // Received STARTTLS cmd when session is already encrypted. + session_->setSessionEncrypted(true); + session_->setState(SmtpSession::State::SessionInProgress); + EXPECT_CALL(callbacks_, sendReplyDownstream(SmtpUtils::generateResponse( + 502, {5, 5, 1}, "Already running in TLS"))); + + result = session_->handleStarttls(); + + EXPECT_EQ(result, SmtpUtils::Result::Stopped); + EXPECT_EQ(session_->getState(), SmtpSession::State::SessionInProgress); +} + +TEST_F(SmtpSessionTest, TestHandleReset) { + + // received RSET command with arguments. + std::string arg = "arg1234"; + EXPECT_CALL(callbacks_, sendReplyDownstream(SmtpUtils::generateResponse( + 501, {5, 5, 4}, "No params allowed for RSET command"))); + + auto result = session_->handleReset(arg); + + EXPECT_EQ(result, SmtpUtils::Result::Stopped); + testing::Mock::VerifyAndClearExpectations(&callbacks_); + + // RSET command is accepted. + arg = ""; + result = session_->handleReset(arg); + + EXPECT_EQ(result, SmtpUtils::Result::ReadyForNext); + EXPECT_TRUE(session_->isCommandInProgress()); + EXPECT_EQ(session_->getCurrentCommand()->getName(), SmtpUtils::smtpRsetCommand); + + // Received RSET when transaction is in progress. + session_->setState(SmtpSession::State::SessionInProgress); + session_->createNewTransaction(); + + arg = ""; + result = session_->handleReset(arg); + + EXPECT_EQ(result, SmtpUtils::Result::ReadyForNext); + EXPECT_EQ(session_->getTransactionState(), SmtpTransaction::State::TransactionAbortRequest); +} + +TEST_F(SmtpSessionTest, TestHandleQuit) { + + // received QUIT command with arguments. + std::string arg = "arg1234"; + EXPECT_CALL(callbacks_, sendReplyDownstream(SmtpUtils::generateResponse( + 501, {5, 5, 4}, "No params allowed for QUIT command"))); + + auto result = session_->handleQuit(arg); + + EXPECT_EQ(result, SmtpUtils::Result::Stopped); + testing::Mock::VerifyAndClearExpectations(&callbacks_); + + // QUIT command is accepted. + arg = ""; + result = session_->handleQuit(arg); + + EXPECT_EQ(result, SmtpUtils::Result::ReadyForNext); + EXPECT_TRUE(session_->isCommandInProgress()); + EXPECT_EQ(session_->getCurrentCommand()->getName(), SmtpUtils::smtpQuitCommand); + EXPECT_EQ(session_->getState(), SmtpSession::State::SessionTerminationRequest); + + // Received QUIT when transaction is in progress. + session_->setState(SmtpSession::State::SessionInProgress); + session_->createNewTransaction(); + arg = ""; + result = session_->handleQuit(arg); + + EXPECT_EQ(result, SmtpUtils::Result::ReadyForNext); + EXPECT_EQ(session_->getState(), SmtpSession::State::SessionTerminationRequest); + EXPECT_EQ(session_->getTransactionState(), SmtpTransaction::State::TransactionAbortRequest); +} + +TEST_F(SmtpSessionTest, TestHandleOtherCmds) { + std::string cmd = SmtpUtils::xReqIdCommand; + + auto result = session_->handleOtherCmds(cmd); + EXPECT_EQ(result, SmtpUtils::Result::ReadyForNext); + EXPECT_TRUE(session_->isCommandInProgress()); + EXPECT_EQ(session_->getCurrentCommand()->getName(), SmtpUtils::xReqIdCommand); +} + +TEST_F(SmtpSessionTest, TestHandleConnResponse) { + uint16_t response_code = 220; + std::string response = "220 localhost ESMTP Service Ready"; + + // Test Connection Success, tracing is not enabled. + // ASSERT_FALSE(callbacks_.tracingEnabled()); + ON_CALL(callbacks_, tracingEnabled()).WillByDefault(Return(false)); + // EXPECT_CALL(callbacks_, tracingEnabled()).WillOnce(Return(false)); + + auto result = session_->handleConnResponse(response_code, response); + EXPECT_EQ(result, SmtpUtils::Result::ReadyForNext); + EXPECT_EQ(session_->getState(), SmtpSession::State::ConnectionSuccess); + + // Test Tracing Enabled + + EXPECT_CALL(callbacks_, tracingEnabled()).WillOnce(Return(true)); + EXPECT_CALL(callbacks_, sendUpstream(_)).WillOnce(Return(true)); + + result = session_->handleConnResponse(response_code, response); + EXPECT_EQ(result, SmtpUtils::Result::Stopped); + EXPECT_EQ(session_->getState(), SmtpSession::State::XReqIdTransfer); + EXPECT_EQ(session_->getResponseOnHold(), response + SmtpUtils::smtpCrlfSuffix); + EXPECT_EQ(session_->getCurrentCommand()->getName(), SmtpUtils::xReqIdCommand); + testing::Mock::VerifyAndClearExpectations(&callbacks_); + + // Test 5xx Error Response + response_code = 554; + EXPECT_CALL(callbacks_, incSmtpConnectionEstablishmentErrors()); + result = session_->handleConnResponse(response_code, response); + EXPECT_EQ(result, SmtpUtils::Result::ReadyForNext); +} + +TEST_F(SmtpSessionTest, TestHandleEhloResponse) { + uint16_t response_code = 250; + std::string response = "250-smtp.example.com\r\n250-PIPELINING\r\n250-SIZE 10240000\r\n"; + session_->newCommand(SmtpUtils::smtpEhloCommand, SmtpCommand::Type::NonTransactionCommand); + + SmtpUtils::Result result = session_->handleEhloResponse(response_code, response); + + EXPECT_EQ(result, SmtpUtils::Result::ReadyForNext); + EXPECT_EQ(session_->getState(), SmtpSession::State::SessionInProgress); + EXPECT_FALSE(session_->isCommandInProgress()); + EXPECT_EQ(session_->getCurrentCommand()->getResponseCode(), response_code); + + // Test case where response code is not 250. + response_code = 500; + response = "500 Internal Server Error\r\n"; + result = session_->handleEhloResponse(response_code, response); + EXPECT_EQ(result, SmtpUtils::Result::ReadyForNext); + EXPECT_EQ(session_->getState(), SmtpSession::State::ConnectionSuccess); + EXPECT_EQ(session_->getCurrentCommand()->getResponseCode(), response_code); +} + +TEST_F(SmtpSessionTest, TestHandleDownstreamTls) { + + // downstreamStartTls returns false, i.e. downstream tls is successful + EXPECT_CALL(callbacks_, downstreamStartTls(SmtpUtils::readyToStartTlsResponse)) + .WillOnce(testing::Return(false)); + EXPECT_CALL(callbacks_, incTlsTerminatedSessions()); + session_->handleDownstreamTls(); + EXPECT_EQ(session_->isSessionEncrypted(), true); + EXPECT_EQ(session_->getState(), SmtpSession::State::SessionInProgress); + testing::Mock::VerifyAndClearExpectations(&callbacks_); + + // downstreamStartTls returns true, i.e. when downstream tls failed + session_->setSessionEncrypted(false); + EXPECT_CALL(callbacks_, downstreamStartTls(SmtpUtils::readyToStartTlsResponse)) + .WillOnce(testing::Return(true)); + EXPECT_CALL(callbacks_, incTlsTerminationErrors()); + EXPECT_CALL(callbacks_, sendReplyDownstream(SmtpUtils::tlsHandshakeErrorResponse)); + // EXPECT_CALL(callbacks_, closeDownstreamConnection()); + session_->handleDownstreamTls(); + EXPECT_EQ(session_->isSessionEncrypted(), false); + EXPECT_EQ(session_->getState(), SmtpSession::State::SessionTerminated); +} + +} // namespace SmtpProxy +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy \ No newline at end of file diff --git a/contrib/smtp_proxy/filters/network/test/smtp_test_config.yaml b/contrib/smtp_proxy/filters/network/test/smtp_test_config.yaml new file mode 100644 index 000000000000..e869100ba440 --- /dev/null +++ b/contrib/smtp_proxy/filters/network/test/smtp_test_config.yaml @@ -0,0 +1,58 @@ +admin: + access_log: + - name: envoy.access_loggers.file + typed_config: + "@type": type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog + path: "{}" + address: + socket_address: + address: "{}" + port_value: 0 +static_resources: + clusters: + name: cluster_0 + connect_timeout: 2s + load_assignment: + cluster_name: cluster_0 + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: "{}" + port_value: 25 + transport_socket: + name: envoy.transport_sockets.starttls + typed_config: + "@type": type.googleapis.com/envoy.extensions.transport_sockets.starttls.v3.UpstreamStartTlsConfig + tls_socket_config: + common_tls_context: {} + listeners: + name: listener_0 + address: + socket_address: + address: "{}" + port_value: 0 + filter_chains: + - filters: + - name: envoy.filters.network.smtp_proxy + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.smtp_proxy.v3alpha.SmtpProxy + stat_prefix: smtp_stats + upstream_tls: REQUIRE + - name: tcp + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy + stat_prefix: tcp_stats + cluster: cluster_0 + transport_socket: + name: envoy.transport_sockets.starttls + typed_config: + "@type": type.googleapis.com/envoy.extensions.transport_sockets.starttls.v3.StartTlsConfig + tls_socket_config: + common_tls_context: + tls_certificates: + - certificate_chain: + filename: "server-cert.pem" + private_key: + filename: "key.pem" diff --git a/contrib/smtp_proxy/filters/network/test/smtp_test_utils.h b/contrib/smtp_proxy/filters/network/test/smtp_test_utils.h new file mode 100644 index 000000000000..609a5eaceaee --- /dev/null +++ b/contrib/smtp_proxy/filters/network/test/smtp_test_utils.h @@ -0,0 +1,71 @@ +#pragma once + +#include "test/mocks/buffer/mocks.h" +#include "test/mocks/network/mocks.h" +#include "test/mocks/stream_info/mocks.h" + +#include "contrib/smtp_proxy/filters/network/source/smtp_decoder.h" + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace SmtpProxy { + +class MockDecoderCallbacks : public DecoderCallbacks { +public: + MockDecoderCallbacks() { + ON_CALL(*this, getStreamInfo()).WillByDefault(testing::ReturnRef(stream_info_)); + ON_CALL(*this, sendReplyDownstream(_)).WillByDefault(testing::Return(false)); + ON_CALL(*this, connection()).WillByDefault(testing::ReturnRef(mock_connection_)); + ON_CALL(*this, getReadBuffer()).WillByDefault(testing::ReturnRef(buffer_)); + } + NiceMock mock_connection_; + NiceMock stream_info_; + NiceMock buffer_; + MOCK_METHOD(void, incSmtpTransactions, (), (override)); + MOCK_METHOD(void, incSmtpTransactionsAborted, (), (override)); + MOCK_METHOD(void, incSmtpSessionRequests, (), (override)); + MOCK_METHOD(void, incSmtpConnectionEstablishmentErrors, (), (override)); + MOCK_METHOD(void, incSmtpSessionsCompleted, (), (override)); + MOCK_METHOD(void, incSmtpSessionsTerminated, (), (override)); + MOCK_METHOD(void, incTlsTerminatedSessions, (), (override)); + MOCK_METHOD(void, incTlsTerminationErrors, (), (override)); + MOCK_METHOD(void, incUpstreamTlsSuccess, (), (override)); + MOCK_METHOD(void, incUpstreamTlsFailed, (), (override)); + + MOCK_METHOD(void, incSmtpAuthErrors, (), (override)); + MOCK_METHOD(void, incMailDataTransferErrors, (), (override)); + MOCK_METHOD(void, incMailRcptErrors, (), (override)); + + MOCK_METHOD(bool, downstreamStartTls, (absl::string_view), (override)); + MOCK_METHOD(bool, sendReplyDownstream, (absl::string_view), (override)); + MOCK_METHOD(bool, sendUpstream, (Buffer::Instance&), (override)); + MOCK_METHOD(bool, upstreamTlsRequired, (), (const)); + MOCK_METHOD(bool, upstreamStartTls, (), (override)); + MOCK_METHOD(void, closeDownstreamConnection, (), (override)); + MOCK_METHOD(bool, tracingEnabled, (), (override)); + // MOCK_METHOD(Buffer::OwnedImpl&, getReadBuffer, (), (override)); + MOCK_METHOD(MockBuffer&, getReadBuffer, (), (override)); + // MOCK_METHOD(Buffer::OwnedImpl&, getWriteBuffer, (), (override)); + MOCK_METHOD(MockBuffer&, getWriteBuffer, (), (override)); + + // MOCK_METHOD(Network::Connection&, connection, (), (const)); + MOCK_METHOD(Network::MockConnection&, connection, (), (const)); + // MOCK_METHOD(StreamInfo::StreamInfo&, getStreamInfo, (), (override)); + MOCK_METHOD(StreamInfo::MockStreamInfo&, getStreamInfo, (), (override)); + MOCK_METHOD(void, emitLogEntry, (StreamInfo::StreamInfo&), (override)); +}; + +class SmtpTestUtils { +public: + inline static const char* smtpHeloCommand = "HELO test.com\r\n"; + inline static const char* smtpEhloCommand = "EHLO test.com\r\n"; + inline static const char* smtpAuthCommand = "AUTH PLAIN AHVzZXJuYW1lAHBhc3N3b3Jk"; + inline static const char* smtpMailCommand = "MAIL FROM:\r\n"; + inline static const char* smtpRcptCommand = "RCPT TO:\r\n"; +}; + +} // namespace SmtpProxy +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy diff --git a/envoy/tcp/upstream.h b/envoy/tcp/upstream.h index a46e37d7141a..e5818e07e624 100644 --- a/envoy/tcp/upstream.h +++ b/envoy/tcp/upstream.h @@ -144,6 +144,11 @@ class GenericUpstream { * to secure mode. Implemented only by start_tls transport socket. */ virtual bool startUpstreamSecureTransport() PURE; + + /* Called when upstream starttls socket is converted to tls and upstream ssl info + * needs to be set in connection's stream_info. + */ + virtual Ssl::ConnectionInfoConstSharedPtr getUpstreamConnectionSslInfo() PURE; }; using GenericConnPoolPtr = std::unique_ptr; diff --git a/source/common/tcp_proxy/tcp_proxy.cc b/source/common/tcp_proxy/tcp_proxy.cc index aacec8314933..76d2fc394305 100644 --- a/source/common/tcp_proxy/tcp_proxy.cc +++ b/source/common/tcp_proxy/tcp_proxy.cc @@ -679,7 +679,14 @@ Network::FilterStatus Filter::onNewConnection() { return establishUpstreamConnection(); } -bool Filter::startUpstreamSecureTransport() { return upstream_->startUpstreamSecureTransport(); } +bool Filter::startUpstreamSecureTransport() { + bool switched_to_tls = upstream_->startUpstreamSecureTransport(); + if (switched_to_tls) { + StreamInfo::UpstreamInfo& upstream_info = *getStreamInfo().upstreamInfo(); + upstream_info.setUpstreamSslConnection(upstream_->getUpstreamConnectionSslInfo()); + } + return switched_to_tls; +} void Filter::onDownstreamEvent(Network::ConnectionEvent event) { if (event == Network::ConnectionEvent::LocalClose || diff --git a/source/common/tcp_proxy/upstream.cc b/source/common/tcp_proxy/upstream.cc index a2dca26b31f5..b3280c53bc6b 100644 --- a/source/common/tcp_proxy/upstream.cc +++ b/source/common/tcp_proxy/upstream.cc @@ -49,6 +49,13 @@ bool TcpUpstream::startUpstreamSecureTransport() { : upstream_conn_data_->connection().startSecureTransport(); } +Ssl::ConnectionInfoConstSharedPtr TcpUpstream::getUpstreamConnectionSslInfo() { + if (!upstream_conn_data_) { + return nullptr; + } + return upstream_conn_data_->connection().ssl(); +} + Tcp::ConnectionPool::ConnectionData* TcpUpstream::onDownstreamEvent(Network::ConnectionEvent event) { if (event == Network::ConnectionEvent::RemoteClose) { diff --git a/source/common/tcp_proxy/upstream.h b/source/common/tcp_proxy/upstream.h index 452df36f376a..2cdf57cc83b0 100644 --- a/source/common/tcp_proxy/upstream.h +++ b/source/common/tcp_proxy/upstream.h @@ -117,6 +117,7 @@ class TcpUpstream : public GenericUpstream { void addBytesSentCallback(Network::Connection::BytesSentCb cb) override; Tcp::ConnectionPool::ConnectionData* onDownstreamEvent(Network::ConnectionEvent event) override; bool startUpstreamSecureTransport() override; + Ssl::ConnectionInfoConstSharedPtr getUpstreamConnectionSslInfo() override; private: Tcp::ConnectionPool::ConnectionDataPtr upstream_conn_data_; @@ -153,6 +154,7 @@ class HttpUpstream : public GenericUpstream, protected Http::StreamCallbacks { void setConnPoolCallbacks(std::unique_ptr&& callbacks) { conn_pool_callbacks_ = std::move(callbacks); } + Ssl::ConnectionInfoConstSharedPtr getUpstreamConnectionSslInfo() override { return nullptr; } protected: HttpUpstream(Tcp::ConnectionPool::UpstreamCallbacks& callbacks, diff --git a/source/extensions/extensions_build_config.bzl b/source/extensions/extensions_build_config.bzl index 19c865886288..7ca5fca0e15e 100644 --- a/source/extensions/extensions_build_config.bzl +++ b/source/extensions/extensions_build_config.bzl @@ -30,12 +30,12 @@ EXTENSIONS = { # Compression # - "envoy.compression.gzip.compressor": "//source/extensions/compression/gzip/compressor:config", - "envoy.compression.gzip.decompressor": "//source/extensions/compression/gzip/decompressor:config", - "envoy.compression.brotli.compressor": "//source/extensions/compression/brotli/compressor:config", - "envoy.compression.brotli.decompressor": "//source/extensions/compression/brotli/decompressor:config", - "envoy.compression.zstd.compressor": "//source/extensions/compression/zstd/compressor:config", - "envoy.compression.zstd.decompressor": "//source/extensions/compression/zstd/decompressor:config", + # "envoy.compression.gzip.compressor": "//source/extensions/compression/gzip/compressor:config", + # "envoy.compression.gzip.decompressor": "//source/extensions/compression/gzip/decompressor:config", + # "envoy.compression.brotli.compressor": "//source/extensions/compression/brotli/compressor:config", + # "envoy.compression.brotli.decompressor": "//source/extensions/compression/brotli/decompressor:config", + # "envoy.compression.zstd.compressor": "//source/extensions/compression/zstd/compressor:config", + # "envoy.compression.zstd.decompressor": "//source/extensions/compression/zstd/decompressor:config", # # Config validators @@ -47,14 +47,14 @@ EXTENSIONS = { # gRPC Credentials Plugins # - "envoy.grpc_credentials.file_based_metadata": "//source/extensions/grpc_credentials/file_based_metadata:config", - "envoy.grpc_credentials.aws_iam": "//source/extensions/grpc_credentials/aws_iam:config", + # "envoy.grpc_credentials.file_based_metadata": "//source/extensions/grpc_credentials/file_based_metadata:config", + # "envoy.grpc_credentials.aws_iam": "//source/extensions/grpc_credentials/aws_iam:config", # # WASM # - "envoy.bootstrap.wasm": "//source/extensions/bootstrap/wasm:config", + # "envoy.bootstrap.wasm": "//source/extensions/bootstrap/wasm:config", # # Health checkers @@ -170,28 +170,28 @@ EXTENSIONS = { "envoy.filters.network.connection_limit": "//source/extensions/filters/network/connection_limit:config", "envoy.filters.network.direct_response": "//source/extensions/filters/network/direct_response:config", - "envoy.filters.network.dubbo_proxy": "//source/extensions/filters/network/dubbo_proxy:config", - "envoy.filters.network.echo": "//source/extensions/filters/network/echo:config", + # "envoy.filters.network.dubbo_proxy": "//source/extensions/filters/network/dubbo_proxy:config", + # "envoy.filters.network.echo": "//source/extensions/filters/network/echo:config", "envoy.filters.network.ext_authz": "//source/extensions/filters/network/ext_authz:config", "envoy.filters.network.http_connection_manager": "//source/extensions/filters/network/http_connection_manager:config", "envoy.filters.network.local_ratelimit": "//source/extensions/filters/network/local_ratelimit:config", - "envoy.filters.network.mongo_proxy": "//source/extensions/filters/network/mongo_proxy:config", + # "envoy.filters.network.mongo_proxy": "//source/extensions/filters/network/mongo_proxy:config", "envoy.filters.network.ratelimit": "//source/extensions/filters/network/ratelimit:config", "envoy.filters.network.rbac": "//source/extensions/filters/network/rbac:config", - "envoy.filters.network.redis_proxy": "//source/extensions/filters/network/redis_proxy:config", + # "envoy.filters.network.redis_proxy": "//source/extensions/filters/network/redis_proxy:config", "envoy.filters.network.tcp_proxy": "//source/extensions/filters/network/tcp_proxy:config", "envoy.filters.network.thrift_proxy": "//source/extensions/filters/network/thrift_proxy:config", "envoy.filters.network.sni_cluster": "//source/extensions/filters/network/sni_cluster:config", "envoy.filters.network.sni_dynamic_forward_proxy": "//source/extensions/filters/network/sni_dynamic_forward_proxy:config", - "envoy.filters.network.wasm": "//source/extensions/filters/network/wasm:config", - "envoy.filters.network.zookeeper_proxy": "//source/extensions/filters/network/zookeeper_proxy:config", + # "envoy.filters.network.wasm": "//source/extensions/filters/network/wasm:config", + # "envoy.filters.network.zookeeper_proxy": "//source/extensions/filters/network/zookeeper_proxy:config", # # UDP filters # - "envoy.filters.udp.dns_filter": "//source/extensions/filters/udp/dns_filter:config", - "envoy.filters.udp_listener.udp_proxy": "//source/extensions/filters/udp/udp_proxy:config", + # "envoy.filters.udp.dns_filter": "//source/extensions/filters/udp/dns_filter:config", + # "envoy.filters.udp_listener.udp_proxy": "//source/extensions/filters/udp/udp_proxy:config", # # Resource monitors @@ -216,21 +216,21 @@ EXTENSIONS = { # Thrift filters # - "envoy.filters.thrift.router": "//source/extensions/filters/network/thrift_proxy/router:config", - "envoy.filters.thrift.header_to_metadata": "//source/extensions/filters/network/thrift_proxy/filters/header_to_metadata:config", - "envoy.filters.thrift.payload_to_metadata": "//source/extensions/filters/network/thrift_proxy/filters/payload_to_metadata:config", - "envoy.filters.thrift.rate_limit": "//source/extensions/filters/network/thrift_proxy/filters/ratelimit:config", + # "envoy.filters.thrift.router": "//source/extensions/filters/network/thrift_proxy/router:config", + # "envoy.filters.thrift.header_to_metadata": "//source/extensions/filters/network/thrift_proxy/filters/header_to_metadata:config", + # "envoy.filters.thrift.payload_to_metadata": "//source/extensions/filters/network/thrift_proxy/filters/payload_to_metadata:config", + # "envoy.filters.thrift.rate_limit": "//source/extensions/filters/network/thrift_proxy/filters/ratelimit:config", # # Tracers # - "envoy.tracers.dynamic_ot": "//source/extensions/tracers/dynamic_ot:config", - "envoy.tracers.datadog": "//source/extensions/tracers/datadog:config", - "envoy.tracers.zipkin": "//source/extensions/tracers/zipkin:config", - "envoy.tracers.opencensus": "//source/extensions/tracers/opencensus:config", - "envoy.tracers.xray": "//source/extensions/tracers/xray:config", - "envoy.tracers.skywalking": "//source/extensions/tracers/skywalking:config", + # "envoy.tracers.dynamic_ot": "//source/extensions/tracers/dynamic_ot:config", + # "envoy.tracers.datadog": "//source/extensions/tracers/datadog:config", + # "envoy.tracers.zipkin": "//source/extensions/tracers/zipkin:config", + # "envoy.tracers.opencensus": "//source/extensions/tracers/opencensus:config", + # "envoy.tracers.xray": "//source/extensions/tracers/xray:config", + # "envoy.tracers.skywalking": "//source/extensions/tracers/skywalking:config", "envoy.tracers.opentelemetry": "//source/extensions/tracers/opentelemetry:config", # @@ -291,11 +291,11 @@ EXTENSIONS = { # WebAssembly runtimes # - "envoy.wasm.runtime.null": "//source/extensions/wasm_runtime/null:config", - "envoy.wasm.runtime.v8": "//source/extensions/wasm_runtime/v8:config", - "envoy.wasm.runtime.wamr": "//source/extensions/wasm_runtime/wamr:config", - "envoy.wasm.runtime.wavm": "//source/extensions/wasm_runtime/wavm:config", - "envoy.wasm.runtime.wasmtime": "//source/extensions/wasm_runtime/wasmtime:config", + # "envoy.wasm.runtime.null": "//source/extensions/wasm_runtime/null:config", + # "envoy.wasm.runtime.v8": "//source/extensions/wasm_runtime/v8:config", + # "envoy.wasm.runtime.wamr": "//source/extensions/wasm_runtime/wamr:config", + # "envoy.wasm.runtime.wavm": "//source/extensions/wasm_runtime/wavm:config", + # "envoy.wasm.runtime.wasmtime": "//source/extensions/wasm_runtime/wasmtime:config", # # Rate limit descriptors @@ -347,16 +347,16 @@ EXTENSIONS = { # QUIC extensions # - "envoy.quic.deterministic_connection_id_generator": "//source/extensions/quic/connection_id_generator:envoy_deterministic_connection_id_generator_config", - "envoy.quic.crypto_stream.server.quiche": "//source/extensions/quic/crypto_stream:envoy_quic_default_crypto_server_stream", - "envoy.quic.proof_source.filter_chain": "//source/extensions/quic/proof_source:envoy_quic_default_proof_source", - "envoy.quic.server_preferred_address.fixed": "//source/extensions/quic/server_preferred_address:fixed_server_preferred_address_config_factory_config", + # "envoy.quic.deterministic_connection_id_generator": "//source/extensions/quic/connection_id_generator:envoy_deterministic_connection_id_generator_config", + # "envoy.quic.crypto_stream.server.quiche": "//source/extensions/quic/crypto_stream:envoy_quic_default_crypto_server_stream", + # "envoy.quic.proof_source.filter_chain": "//source/extensions/quic/proof_source:envoy_quic_default_proof_source", + # "envoy.quic.server_preferred_address.fixed": "//source/extensions/quic/server_preferred_address:fixed_server_preferred_address_config_factory_config", # # UDP packet writers # - "envoy.udp_packet_writer.default": "//source/extensions/udp_packet_writer/default:config", - "envoy.udp_packet_writer.gso": "//source/extensions/udp_packet_writer/gso:config", + # "envoy.udp_packet_writer.default": "//source/extensions/udp_packet_writer/default:config", + # "envoy.udp_packet_writer.gso": "//source/extensions/udp_packet_writer/gso:config", # # Formatter diff --git a/source/extensions/filters/network/well_known_names.h b/source/extensions/filters/network/well_known_names.h index 0017db6f1ff7..4b935cdd8061 100644 --- a/source/extensions/filters/network/well_known_names.h +++ b/source/extensions/filters/network/well_known_names.h @@ -59,6 +59,8 @@ class NetworkFilterNameValues { const std::string ZooKeeperProxy = "envoy.filters.network.zookeeper_proxy"; // WebAssembly filter const std::string Wasm = "envoy.filters.network.wasm"; + // SMTP proxy filter + const std::string SmtpProxy = "envoy.filters.network.smtp_proxy"; }; using NetworkFilterNames = ConstSingleton; diff --git a/source/extensions/transport_sockets/proxy_protocol/proxy_protocol.h b/source/extensions/transport_sockets/proxy_protocol/proxy_protocol.h index 1a92423c9a1c..a052f807fcc2 100644 --- a/source/extensions/transport_sockets/proxy_protocol/proxy_protocol.h +++ b/source/extensions/transport_sockets/proxy_protocol/proxy_protocol.h @@ -38,6 +38,7 @@ class UpstreamProxyProtocolSocket : public TransportSockets::PassthroughSocket, void setTransportSocketCallbacks(Network::TransportSocketCallbacks& callbacks) override; Network::IoResult doWrite(Buffer::Instance& buffer, bool end_stream) override; void onConnected() override; + bool startSecureTransport() override { return transport_socket_->startSecureTransport(); } private: void generateHeader(); diff --git a/source/extensions/transport_sockets/starttls/starttls_socket.cc b/source/extensions/transport_sockets/starttls/starttls_socket.cc index 38e2448f23e2..71a521ac8991 100644 --- a/source/extensions/transport_sockets/starttls/starttls_socket.cc +++ b/source/extensions/transport_sockets/starttls/starttls_socket.cc @@ -17,6 +17,8 @@ bool StartTlsSocket::startSecureTransport() { // does buffering, it should be flushed before destroying or // flush should be called from destructor. active_socket_ = std::move(tls_socket_); + + callbacks_.connection().connectionInfoSetter().setSslConnection(active_socket_->ssl()); using_tls_ = true; } return true; diff --git a/test/extensions/transport_sockets/proxy_protocol/BUILD b/test/extensions/transport_sockets/proxy_protocol/BUILD index cacd476c36e7..6c9e82df4eee 100644 --- a/test/extensions/transport_sockets/proxy_protocol/BUILD +++ b/test/extensions/transport_sockets/proxy_protocol/BUILD @@ -19,6 +19,7 @@ envoy_extension_cc_test( "//envoy/network:proxy_protocol_options_lib", "//source/extensions/common/proxy_protocol:proxy_protocol_header_lib", "//source/extensions/transport_sockets/proxy_protocol:upstream_proxy_protocol", + "//source/extensions/transport_sockets/starttls:config", "//test/mocks/buffer:buffer_mocks", "//test/mocks/network:io_handle_mocks", "//test/mocks/network:network_mocks", diff --git a/test/extensions/transport_sockets/proxy_protocol/proxy_protocol_test.cc b/test/extensions/transport_sockets/proxy_protocol/proxy_protocol_test.cc index 53d1646b50f9..d834981164ec 100644 --- a/test/extensions/transport_sockets/proxy_protocol/proxy_protocol_test.cc +++ b/test/extensions/transport_sockets/proxy_protocol/proxy_protocol_test.cc @@ -6,6 +6,7 @@ #include "source/common/network/transport_socket_options_impl.h" #include "source/extensions/common/proxy_protocol/proxy_protocol_header.h" #include "source/extensions/transport_sockets/proxy_protocol/proxy_protocol.h" +#include "source/extensions/transport_sockets/starttls/starttls_socket.h" #include "test/mocks/buffer/mocks.h" #include "test/mocks/network/io_handle.h" @@ -53,6 +54,52 @@ class ProxyProtocolTest : public testing::Test { Stats::TestUtil::TestStore stats_store_; }; +// Test startSecureTransport() api call on UpstreamProxyProtocolSocket +TEST_F(ProxyProtocolTest, TestStartSecureTransportCall) { + ProxyProtocolConfig config; + config.set_version(ProxyProtocolConfig_Version::ProxyProtocolConfig_Version_V2); + initialize(config, nullptr); + + EXPECT_CALL(*inner_socket_, startSecureTransport()).WillOnce(Return(false)); + proxy_protocol_socket_->startSecureTransport(); +} + +// When Inner socket is of starttls type +TEST_F(ProxyProtocolTest, TestInnerStarttlsSocket) { + // Initialize starttls socket + Network::TransportSocketOptionsConstSharedPtr socket_options = + std::make_shared(); + NiceMock transport_callbacks; + Network::MockTransportSocket* raw_socket = new Network::MockTransportSocket; + Network::MockTransportSocket* ssl_socket = new Network::MockTransportSocket; + + std::unique_ptr inner_socket = + std::make_unique(Network::TransportSocketPtr(raw_socket), + Network::TransportSocketPtr(ssl_socket), + socket_options); + + // Initialize proxy protocol socket + ProxyProtocolConfig config; + config.set_version(ProxyProtocolConfig_Version::ProxyProtocolConfig_Version_V2); + + NiceMock proxyproto_transport_callbacks; + proxy_protocol_socket_ = std::make_unique( + std::move(inner_socket), nullptr, config, *stats_store_.rootScope()); + + // Starttls socket is initial raw_socket. + EXPECT_CALL(*raw_socket, setTransportSocketCallbacks(_)); + proxy_protocol_socket_->setTransportSocketCallbacks(proxyproto_transport_callbacks); + + EXPECT_CALL(*raw_socket, onConnected()); + proxy_protocol_socket_->onConnected(); + + // Now switch to secure transport i.e. TLS + EXPECT_CALL(*ssl_socket, ssl()); + EXPECT_CALL(*ssl_socket, setTransportSocketCallbacks(_)); + EXPECT_CALL(*ssl_socket, onConnected()); + proxy_protocol_socket_->startSecureTransport(); +} + // Test injects PROXY protocol header only once TEST_F(ProxyProtocolTest, InjectesHeaderOnlyOnce) { transport_callbacks_.connection_.stream_info_.downstream_connection_info_provider_ diff --git a/test/extensions/transport_sockets/starttls/starttls_socket_test.cc b/test/extensions/transport_sockets/starttls/starttls_socket_test.cc index 33e5757f5d8b..68cc4eb62c1b 100644 --- a/test/extensions/transport_sockets/starttls/starttls_socket_test.cc +++ b/test/extensions/transport_sockets/starttls/starttls_socket_test.cc @@ -72,6 +72,8 @@ TEST(StartTlsTest, BasicSwitch) { // Now switch to Tls. During the switch, the new socket should register for callbacks // and connect. + + EXPECT_CALL(*ssl_socket, ssl()); EXPECT_CALL(*ssl_socket, setTransportSocketCallbacks(_)); EXPECT_CALL(*ssl_socket, onConnected); // Make sure that raw socket is destructed.