diff --git a/src/shared/protocols/protocols.h b/src/shared/protocols/protocols.h index 47b04b74d12..d3b0d7072b8 100644 --- a/src/shared/protocols/protocols.h +++ b/src/shared/protocols/protocols.h @@ -39,6 +39,7 @@ enum class Protocol { kKafka = 10, kMux = 11, kAMQP = 12, + kTLS = 13, }; } // namespace protocols diff --git a/src/stirling/binaries/stirling_wrapper.cc b/src/stirling/binaries/stirling_wrapper.cc index 8bb8ac8bb63..fb5e59126cf 100644 --- a/src/stirling/binaries/stirling_wrapper.cc +++ b/src/stirling/binaries/stirling_wrapper.cc @@ -62,7 +62,7 @@ DEFINE_string(trace, "", "Dynamic trace to deploy. Either (1) the path to a file containing PxL or IR trace " "spec, or (2) : for full-function tracing."); DEFINE_string(print_record_batches, - "http_events,mysql_events,pgsql_events,redis_events,cql_events,dns_events", + "http_events,mysql_events,pgsql_events,redis_events,cql_events,dns_events,tls_events", "Comma-separated list of tables to print."); DEFINE_bool(init_only, false, "If true, only runs the init phase and exits. For testing."); DEFINE_int32(timeout_secs, -1, diff --git a/src/stirling/source_connectors/socket_tracer/BUILD.bazel b/src/stirling/source_connectors/socket_tracer/BUILD.bazel index f738dd92d8c..c552a49ac50 100644 --- a/src/stirling/source_connectors/socket_tracer/BUILD.bazel +++ b/src/stirling/source_connectors/socket_tracer/BUILD.bazel @@ -516,6 +516,27 @@ pl_cc_bpf_test( ], ) +pl_cc_bpf_test( + name = "tls_trace_bpf_test", + timeout = "long", + srcs = ["tls_trace_bpf_test.cc"], + flaky = True, + shard_count = 2, + tags = [ + "cpu:16", + "no_asan", + "requires_bpf", + ], + deps = [ + ":cc_library", + "//src/common/testing/test_utils:cc_library", + "//src/stirling/source_connectors/socket_tracer/testing:cc_library", + "//src/stirling/source_connectors/socket_tracer/testing/container_images:curl_container", + "//src/stirling/source_connectors/socket_tracer/testing/container_images:nginx_openssl_3_0_8_container", + "//src/stirling/testing:cc_library", + ], +) + pl_cc_bpf_test( name = "dyn_lib_trace_bpf_test", timeout = "moderate", diff --git a/src/stirling/source_connectors/socket_tracer/bcc_bpf/BUILD.bazel b/src/stirling/source_connectors/socket_tracer/bcc_bpf/BUILD.bazel index 5d13f478652..1309620ac16 100644 --- a/src/stirling/source_connectors/socket_tracer/bcc_bpf/BUILD.bazel +++ b/src/stirling/source_connectors/socket_tracer/bcc_bpf/BUILD.bazel @@ -82,6 +82,7 @@ pl_cc_test( "ENABLE_NATS_TRACING=true", "ENABLE_MONGO_TRACING=true", "ENABLE_AMQP_TRACING=true", + "ENABLE_TLS_TRACING=true", ], deps = [ "//src/stirling/bpf_tools/bcc_bpf:headers", diff --git a/src/stirling/source_connectors/socket_tracer/bcc_bpf/protocol_inference.h b/src/stirling/source_connectors/socket_tracer/bcc_bpf/protocol_inference.h index 36aedc1d5d2..e6f89610b98 100644 --- a/src/stirling/source_connectors/socket_tracer/bcc_bpf/protocol_inference.h +++ b/src/stirling/source_connectors/socket_tracer/bcc_bpf/protocol_inference.h @@ -60,6 +60,46 @@ static __inline enum message_type_t infer_http_message(const char* buf, size_t c return kUnknown; } +static __inline enum message_type_t infer_tls_message(const char* buf, size_t count) { + if (count < 6) { + return kUnknown; + } + + uint8_t content_type = buf[0]; + // TLS content types correspond to the following: + // 0x14: ChangeCipherSpec + // 0x15: Alert + // 0x16: Handshake + // 0x17: ApplicationData + // 0x18: Heartbeat + if (content_type != 0x16) { + return kUnknown; + } + + uint16_t legacy_version = buf[1] << 8 | buf[2]; + // TLS versions correspond to the following: + // 0x0300: SSL 3.0 + // 0x0301: TLS 1.0 + // 0x0302: TLS 1.1 + // 0x0303: TLS 1.2 + // 0x0304: TLS 1.3 + if (legacy_version < 0x0300 || legacy_version > 0x0304) { + return kUnknown; + } + + uint8_t handshake_type = buf[5]; + // Check for ServerHello + if (handshake_type == 2) { + return kResponse; + } + // Check for ClientHello + if (handshake_type == 1) { + return kRequest; + } + + return kUnknown; +} + // Cassandra frame: // 0 8 16 24 32 40 // +---------+---------+---------+---------+---------+ @@ -699,7 +739,16 @@ static __inline struct protocol_message_t infer_protocol(const char* buf, size_t // role by considering which side called accept() vs connect(). Once the clean-up // above is done, the code below can be turned into a chained ternary. // PROTOCOL_LIST: Requires update on new protocols. - if (ENABLE_HTTP_TRACING && (inferred_message.type = infer_http_message(buf, count)) != kUnknown) { + // + // TODO(ddelnano): TLS tracing should be handled differently in the future as we want to be able + // to trace the handshake and the application data separately (gh#2095). The current connection + // tracker model only works with one or the other, meaning if TLS tracing is enabled, tracing the + // plaintext within an encrypted conn will not work. ENABLE_TLS_TRACING will default to false + // until this is revisted. + if (ENABLE_TLS_TRACING && (inferred_message.type = infer_tls_message(buf, count)) != kUnknown) { + inferred_message.protocol = kProtocolTLS; + } else if (ENABLE_HTTP_TRACING && + (inferred_message.type = infer_http_message(buf, count)) != kUnknown) { inferred_message.protocol = kProtocolHTTP; } else if (ENABLE_CQL_TRACING && (inferred_message.type = infer_cql_message(buf, count)) != kUnknown) { diff --git a/src/stirling/source_connectors/socket_tracer/bcc_bpf/protocol_inference_test.cc b/src/stirling/source_connectors/socket_tracer/bcc_bpf/protocol_inference_test.cc index 0bb30f0e86c..2ad70eac4a5 100644 --- a/src/stirling/source_connectors/socket_tracer/bcc_bpf/protocol_inference_test.cc +++ b/src/stirling/source_connectors/socket_tracer/bcc_bpf/protocol_inference_test.cc @@ -482,3 +482,27 @@ TEST(ProtocolInferenceTest, AMQPResponse) { EXPECT_EQ(protocol_message.protocol, kProtocolAMQP); EXPECT_EQ(protocol_message.type, kResponse); } + +TEST(ProtocolInferenceTest, TLSRequest) { + struct conn_info_t conn_info = {}; + // TLS Client Hello + constexpr uint8_t kReqFrame[] = { + 0x16, 0x03, 0x01, 0x00, 0x01, 0x01, 0x00, 0x01, 0xfc, 0x03, 0x03, 0x7b, 0x7b, 0x7b, + }; + auto protocol_message = + infer_protocol(reinterpret_cast(kReqFrame), sizeof(kReqFrame), &conn_info); + EXPECT_EQ(protocol_message.protocol, kProtocolTLS); + EXPECT_EQ(protocol_message.type, kRequest); +} + +TEST(ProtocolInferenceTest, TLSResponse) { + struct conn_info_t conn_info = {}; + // TLS Server Hello + constexpr uint8_t kRespFrame[] = { + 0x16, 0x03, 0x01, 0x00, 0x01, 0x02, 0x00, 0x00, 0xfc, 0x03, 0x03, 0x7b, 0x7b, 0x7b, + }; + auto protocol_message = + infer_protocol(reinterpret_cast(kRespFrame), sizeof(kRespFrame), &conn_info); + EXPECT_EQ(protocol_message.protocol, kProtocolTLS); + EXPECT_EQ(protocol_message.type, kResponse); +} diff --git a/src/stirling/source_connectors/socket_tracer/bcc_bpf_intf/common.h b/src/stirling/source_connectors/socket_tracer/bcc_bpf_intf/common.h index 18018ea0a85..7277c3f864f 100644 --- a/src/stirling/source_connectors/socket_tracer/bcc_bpf_intf/common.h +++ b/src/stirling/source_connectors/socket_tracer/bcc_bpf_intf/common.h @@ -51,6 +51,7 @@ enum traffic_protocol_t { kProtocolKafka = 10, kProtocolMux = 11, kProtocolAMQP = 12, + kProtocolTLS = 13, // We use magic enum to iterate through protocols in C++ land, // and don't want the C-enum-size trick to show up there. #ifndef __cplusplus diff --git a/src/stirling/source_connectors/socket_tracer/conn_tracker.cc b/src/stirling/source_connectors/socket_tracer/conn_tracker.cc index 3ce7e20a064..35ca7f6fc38 100644 --- a/src/stirling/source_connectors/socket_tracer/conn_tracker.cc +++ b/src/stirling/source_connectors/socket_tracer/conn_tracker.cc @@ -674,6 +674,7 @@ auto CreateTraceRoles() { res.Set(kProtocolKafka, {kRoleServer}); res.Set(kProtocolMux, {kRoleServer}); res.Set(kProtocolAMQP, {kRoleServer}); + res.Set(kProtocolTLS, {kRoleServer}); DCHECK(res.AreAllKeysSet()); return res; diff --git a/src/stirling/source_connectors/socket_tracer/data_stream.cc b/src/stirling/source_connectors/socket_tracer/data_stream.cc index 7a394eb4dc0..d0fda642539 100644 --- a/src/stirling/source_connectors/socket_tracer/data_stream.cc +++ b/src/stirling/source_connectors/socket_tracer/data_stream.cc @@ -215,6 +215,10 @@ template void DataStream::ProcessBytesToFrames( message_type_t type, protocols::mongodb::StateWrapper* state); + +template void DataStream::ProcessBytesToFrames(message_type_t type, + protocols::NoState* state); void DataStream::Reset() { data_buffer_.Reset(); has_new_events_ = false; diff --git a/src/stirling/source_connectors/socket_tracer/protocols/BUILD.bazel b/src/stirling/source_connectors/socket_tracer/protocols/BUILD.bazel index dba40e9a05a..1026cd6945c 100644 --- a/src/stirling/source_connectors/socket_tracer/protocols/BUILD.bazel +++ b/src/stirling/source_connectors/socket_tracer/protocols/BUILD.bazel @@ -46,5 +46,6 @@ pl_cc_library( "//src/stirling/source_connectors/socket_tracer/protocols/nats:cc_library", "//src/stirling/source_connectors/socket_tracer/protocols/pgsql:cc_library", "//src/stirling/source_connectors/socket_tracer/protocols/redis:cc_library", + "//src/stirling/source_connectors/socket_tracer/protocols/tls:cc_library", ], ) diff --git a/src/stirling/source_connectors/socket_tracer/protocols/stitchers.h b/src/stirling/source_connectors/socket_tracer/protocols/stitchers.h index 81cc490ec06..bbdbd2449c0 100644 --- a/src/stirling/source_connectors/socket_tracer/protocols/stitchers.h +++ b/src/stirling/source_connectors/socket_tracer/protocols/stitchers.h @@ -31,3 +31,4 @@ #include "src/stirling/source_connectors/socket_tracer/protocols/nats/stitcher.h" // IWYU pragma: export #include "src/stirling/source_connectors/socket_tracer/protocols/pgsql/stitcher.h" // IWYU pragma: export #include "src/stirling/source_connectors/socket_tracer/protocols/redis/stitcher.h" // IWYU pragma: export +#include "src/stirling/source_connectors/socket_tracer/protocols/tls/stitcher.h" // IWYU pragma: export diff --git a/src/stirling/source_connectors/socket_tracer/protocols/tls/parse.cc b/src/stirling/source_connectors/socket_tracer/protocols/tls/parse.cc index 93d90cfa6ea..9a2d693d09d 100644 --- a/src/stirling/source_connectors/socket_tracer/protocols/tls/parse.cc +++ b/src/stirling/source_connectors/socket_tracer/protocols/tls/parse.cc @@ -18,6 +18,7 @@ #include "src/stirling/source_connectors/socket_tracer/protocols/tls/parse.h" #include +#include #include #include #include @@ -31,6 +32,8 @@ namespace stirling { namespace protocols { namespace tls { +using px::utils::JSONObjectBuilder; + constexpr size_t kTLSRecordHeaderLength = 5; constexpr size_t kExtensionMinimumLength = 4; constexpr size_t kSNIExtensionMinimumLength = 3; @@ -39,11 +42,9 @@ constexpr size_t kSNIExtensionMinimumLength = 3; // In TLS 1.2 and earlier, gmt_unix_time is 4 bytes and Random is 28 bytes. constexpr size_t kRandomStructLength = 32; -StatusOr ExtractSNIExtension(std::map* exts, - BinaryDecoder* decoder) { +StatusOr ExtractSNIExtension(SharedExtensions* exts, BinaryDecoder* decoder) { PX_ASSIGN_OR(auto server_name_list_length, decoder->ExtractBEInt(), return ParseState::kInvalid); - std::vector server_names; while (server_name_list_length > 0) { PX_ASSIGN_OR(auto server_name_type, decoder->ExtractBEInt(), return error::Internal("Failed to extract server name type")); @@ -56,10 +57,9 @@ StatusOr ExtractSNIExtension(std::map* ext PX_ASSIGN_OR(auto server_name, decoder->ExtractString(server_name_length), return error::Internal("Failed to extract server name")); - server_names.push_back(std::string(server_name)); + exts->server_names.push_back(std::string(server_name)); server_name_list_length -= kSNIExtensionMinimumLength + server_name_length; } - exts->insert({"server_name", ToJSONString(server_names)}); return ParseState::kSuccess; } @@ -76,7 +76,7 @@ StatusOr ExtractSNIExtension(std::map* ext * diagram: https://en.wikipedia.org/wiki/Transport_Layer_Security#TLS_record */ -ParseState ParseFullFrame(BinaryDecoder* decoder, Frame* frame) { +ParseState ParseFullFrame(SharedExtensions* extensions, BinaryDecoder* decoder, Frame* frame) { PX_ASSIGN_OR(auto raw_content_type, decoder->ExtractBEInt(), return ParseState::kInvalid); auto content_type = magic_enum::enum_cast(raw_content_type); @@ -170,7 +170,7 @@ ParseState ParseFullFrame(BinaryDecoder* decoder, Frame* frame) { if (extension_length > 0) { if (extension_type == 0x00) { - if (!ExtractSNIExtension(&frame->extensions, decoder).ok()) { + if (!ExtractSNIExtension(extensions, decoder).ok()) { return ParseState::kInvalid; } } else { @@ -182,6 +182,9 @@ ParseState ParseFullFrame(BinaryDecoder* decoder, Frame* frame) { extensions_length -= kExtensionMinimumLength + extension_length; } + JSONObjectBuilder body_builder; + body_builder.WriteKVRecursive("extensions", *extensions); + frame->body = body_builder.GetString(); return ParseState::kSuccess; } @@ -189,7 +192,7 @@ ParseState ParseFullFrame(BinaryDecoder* decoder, Frame* frame) { } // namespace tls template <> -ParseState ParseFrame(message_type_t, std::string_view* buf, tls::Frame* frame, NoState*) { +ParseState ParseFrame(message_type_t type, std::string_view* buf, tls::Frame* frame, NoState*) { // TLS record header is 5 bytes. The size of the record is in bytes 4 and 5. if (buf->length() < tls::kTLSRecordHeaderLength) { return ParseState::kNeedsMoreData; @@ -200,7 +203,13 @@ ParseState ParseFrame(message_type_t, std::string_view* buf, tls::Frame* frame, } BinaryDecoder decoder(*buf); - auto parse_result = tls::ParseFullFrame(&decoder, frame); + std::unique_ptr extensions; + if (type == kRequest) { + extensions = std::make_unique(); + } else { + extensions = std::make_unique(); + } + auto parse_result = tls::ParseFullFrame(extensions.get(), &decoder, frame); if (parse_result == ParseState::kSuccess) { buf->remove_prefix(length + tls::kTLSRecordHeaderLength); } diff --git a/src/stirling/source_connectors/socket_tracer/protocols/tls/parse.h b/src/stirling/source_connectors/socket_tracer/protocols/tls/parse.h index 12e5bdbe29d..70ca25bd6e6 100644 --- a/src/stirling/source_connectors/socket_tracer/protocols/tls/parse.h +++ b/src/stirling/source_connectors/socket_tracer/protocols/tls/parse.h @@ -28,7 +28,7 @@ namespace stirling { namespace protocols { namespace tls { -ParseState ParseFullFrame(BinaryDecoder* decoder, Frame* frame); +ParseState ParseFullFrame(SharedExtensions* extensions, BinaryDecoder* decoder, Frame* frame); } diff --git a/src/stirling/source_connectors/socket_tracer/protocols/tls/parse_test.cc b/src/stirling/source_connectors/socket_tracer/protocols/tls/parse_test.cc index bbffb9618f7..9dc84c46a60 100644 --- a/src/stirling/source_connectors/socket_tracer/protocols/tls/parse_test.cc +++ b/src/stirling/source_connectors/socket_tracer/protocols/tls/parse_test.cc @@ -315,8 +315,7 @@ TEST_F(TLSParserTest, ParseValidClientHello) { ASSERT_GT(frame.session_id.size(), 0); // Validate the SNI extension was parsed properly - ASSERT_EQ(frame.extensions.size(), 1); - ASSERT_EQ(frame.extensions["server_name"], "[\"argocd-cluster-repo-server\"]"); + ASSERT_EQ(frame.body, R"({"extensions":{"server_name":["argocd-cluster-repo-server"]}})"); ASSERT_EQ(state, ParseState::kSuccess); } diff --git a/src/stirling/source_connectors/socket_tracer/protocols/tls/types.h b/src/stirling/source_connectors/socket_tracer/protocols/tls/types.h index c64da970554..fdf65e6d95f 100644 --- a/src/stirling/source_connectors/socket_tracer/protocols/tls/types.h +++ b/src/stirling/source_connectors/socket_tracer/protocols/tls/types.h @@ -43,8 +43,6 @@ namespace stirling { namespace protocols { namespace tls { -using ::px::utils::ToJSONString; - enum class ContentType : uint8_t { kChangeCipherSpec = 0x14, kAlert = 0x15, @@ -186,6 +184,28 @@ enum class ExtensionType : uint16_t { kRenegotiationInfo = 65281, }; +// Extensions that are common to both the client and server side +// of a TLS handshake +struct SharedExtensions { + std::vector server_names; + + virtual void ToJSON(::px::utils::JSONObjectBuilder* /*builder*/) const {} + virtual ~SharedExtensions() = default; +}; + +struct ReqExtensions : public SharedExtensions { + void ToJSON(::px::utils::JSONObjectBuilder* builder) const override { + SharedExtensions::ToJSON(builder); + builder->WriteKV("server_name", server_names); + } +}; + +struct RespExtensions : public SharedExtensions { + void ToJSON(::px::utils::JSONObjectBuilder* builder) const override { + SharedExtensions::ToJSON(builder); + } +}; + struct Frame : public FrameBase { ContentType content_type; @@ -195,12 +215,12 @@ struct Frame : public FrameBase { HandshakeType handshake_type; - uint24_t handshake_length; + uint24_t handshake_length = uint24_t(0); LegacyVersion handshake_version; std::string session_id; - std::map extensions; + std::string body; bool consumed = false; @@ -209,9 +229,8 @@ struct Frame : public FrameBase { std::string ToString() const override { return absl::Substitute( "TLS Frame [len=$0 content_type=$1 legacy_version=$2 handshake_version=$3 " - "handshake_type=$4 extensions=$5]", - length, content_type, legacy_version, handshake_version, handshake_type, - ToJSONString(extensions)); + "handshake_type=$4 body=$5]", + length, content_type, legacy_version, handshake_version, handshake_type, body); } }; diff --git a/src/stirling/source_connectors/socket_tracer/protocols/types.h b/src/stirling/source_connectors/socket_tracer/protocols/types.h index 24aa3ffd024..560da8e2433 100644 --- a/src/stirling/source_connectors/socket_tracer/protocols/types.h +++ b/src/stirling/source_connectors/socket_tracer/protocols/types.h @@ -34,6 +34,7 @@ #include "src/stirling/source_connectors/socket_tracer/protocols/nats/types.h" #include "src/stirling/source_connectors/socket_tracer/protocols/pgsql/types.h" #include "src/stirling/source_connectors/socket_tracer/protocols/redis/types.h" +#include "src/stirling/source_connectors/socket_tracer/protocols/tls/types.h" namespace px { namespace stirling { @@ -53,7 +54,8 @@ using FrameDequeVariant = std::variant>, absl::flat_hash_map>, absl::flat_hash_map>, - absl::flat_hash_map>>; + absl::flat_hash_map>, + absl::flat_hash_map>>; // clang-format off } // namespace protocols diff --git a/src/stirling/source_connectors/socket_tracer/socket_trace_connector.cc b/src/stirling/source_connectors/socket_tracer/socket_trace_connector.cc index 5e18e70d504..2a494cc56cd 100644 --- a/src/stirling/source_connectors/socket_tracer/socket_trace_connector.cc +++ b/src/stirling/source_connectors/socket_tracer/socket_trace_connector.cc @@ -117,6 +117,11 @@ DEFINE_int32(stirling_enable_mongodb_tracing, gflags::Int32FromEnv("PX_STIRLING_ENABLE_MONGODB_TRACING", px::stirling::TraceMode::OnForNewerKernel), "If true, stirling will trace and process MongoDB messages"); +DEFINE_int32( + stirling_enable_tls_tracing, + gflags::Int32FromEnv("PX_STIRLING_ENABLE_TLS_TRACING", px::stirling::TraceMode::Off), + "If true, stirling will trace and process TLS protocol (not the TLS payload) messages. Note: " + "this disables tracing the plaintext within encrypted connections until gh#2095 is addressed."); DEFINE_bool(stirling_disable_golang_tls_tracing, gflags::BoolFromEnv("PX_STIRLING_DISABLE_GOLANG_TLS_TRACING", false), "If true, stirling will not trace TLS traffic for Go applications. This implies " @@ -198,6 +203,12 @@ using px::utils::ToJSONString; // Most HTTP servers support 8K headers, so we truncate after that. // https://stackoverflow.com/questions/686217/maximum-on-http-header-values constexpr size_t kMaxHTTPHeadersBytes = 8192; +// TLS records have a maximum size of 16KiB. The bulk of the body columns are extensions +// and while there isn't a size limit for them, we limit it to 1 KiB to avoid excessive +// memory usage. A typical ClientHello from curl is around 500 bytes. This assumes that +// all extensions are captured, but we won't support capturing all extensions and +// will avoid large extensions like the padding extension, +constexpr size_t kMaxTLSBodyBytes = 1024; // Protobuf printer will limit strings to this length. constexpr size_t kMaxPBStringLen = 64; @@ -283,6 +294,10 @@ void SocketTraceConnector::InitProtocolTransferSpecs() { kAMQPTableNum, {kRoleClient, kRoleServer}, TRANSFER_STREAM_PROTOCOL(amqp)}}, + {kProtocolTLS, TransferSpec{FLAGS_stirling_enable_tls_tracing, + kTLSTableNum, + {kRoleClient, kRoleServer}, + TRANSFER_STREAM_PROTOCOL(tls)}}, {kProtocolUnknown, TransferSpec{/* trace_mode */ px::stirling::TraceMode::Off, /* table_num */ static_cast(-1), /* trace_roles */ {}, @@ -491,6 +506,7 @@ Status SocketTraceConnector::InitBPF() { absl::StrCat("-DENABLE_NATS_TRACING=", protocol_transfer_specs_[kProtocolNATS].enabled), absl::StrCat("-DENABLE_AMQP_TRACING=", protocol_transfer_specs_[kProtocolAMQP].enabled), absl::StrCat("-DENABLE_MONGO_TRACING=", protocol_transfer_specs_[kProtocolMongo].enabled), + absl::StrCat("-DENABLE_TLS_TRACING=", protocol_transfer_specs_[kProtocolTLS].enabled), absl::StrCat("-DBPF_LOOP_LIMIT=", FLAGS_stirling_bpf_loop_limit), absl::StrCat("-DBPF_CHUNK_LIMIT=", FLAGS_stirling_bpf_chunk_limit), }; @@ -1686,6 +1702,35 @@ void SocketTraceConnector::AppendMessage(ConnectorContext* ctx, const ConnTracke #endif } +template <> +void SocketTraceConnector::AppendMessage(ConnectorContext* ctx, const ConnTracker& conn_tracker, + protocols::tls::Record record, DataTable* data_table) { + protocols::tls::Frame& req_message = record.req; + protocols::tls::Frame& resp_message = record.resp; + + md::UPID upid(ctx->GetASID(), conn_tracker.conn_id().upid.pid, + conn_tracker.conn_id().upid.start_time_ticks); + + DataTable::RecordBuilder<&kTLSTable> r(data_table, resp_message.timestamp_ns); + r.Append(resp_message.timestamp_ns); + r.Append(upid.value()); + // Note that there is a string copy here, + // But std::move is not allowed because we re-use conn object. + r.Append(conn_tracker.remote_endpoint().AddrStr()); + r.Append(conn_tracker.remote_endpoint().port()); + r.Append(conn_tracker.local_endpoint().AddrStr()); + r.Append(conn_tracker.local_endpoint().port()); + r.Append(conn_tracker.role()); + r.Append(static_cast(req_message.content_type)); + r.Append(req_message.body, kMaxTLSBodyBytes); + r.Append(resp_message.body, kMaxTLSBodyBytes); + r.Append( + CalculateLatency(req_message.timestamp_ns, resp_message.timestamp_ns)); +#ifndef NDEBUG + r.Append(PXInfoString(conn_tracker, record)); +#endif +} + void SocketTraceConnector::SetupOutput(const std::filesystem::path& path) { DCHECK(!path.empty()); diff --git a/src/stirling/source_connectors/socket_tracer/socket_trace_connector.h b/src/stirling/source_connectors/socket_tracer/socket_trace_connector.h index b493cb5b922..2534f57a71e 100644 --- a/src/stirling/source_connectors/socket_tracer/socket_trace_connector.h +++ b/src/stirling/source_connectors/socket_tracer/socket_trace_connector.h @@ -66,6 +66,7 @@ DECLARE_int32(stirling_enable_kafka_tracing); DECLARE_int32(stirling_enable_mux_tracing); DECLARE_int32(stirling_enable_amqp_tracing); DECLARE_int32(stirling_enable_mongodb_tracing); +DECLARE_int32(stirling_enable_tls_tracing); DECLARE_bool(stirling_disable_self_tracing); DECLARE_string(stirling_role_to_trace); @@ -95,9 +96,9 @@ class SocketTraceConnector : public BCCSourceConnector { public: static constexpr std::string_view kName = "socket_tracer"; // PROTOCOL_LIST - static constexpr auto kTables = - MakeArray(kConnStatsTable, kHTTPTable, kMySQLTable, kCQLTable, kPGSQLTable, kDNSTable, - kRedisTable, kNATSTable, kKafkaTable, kMuxTable, kAMQPTable, kMongoDBTable); + static constexpr auto kTables = MakeArray( + kConnStatsTable, kHTTPTable, kMySQLTable, kCQLTable, kPGSQLTable, kDNSTable, kRedisTable, + kNATSTable, kKafkaTable, kMuxTable, kAMQPTable, kMongoDBTable, kTLSTable); static constexpr uint32_t kConnStatsTableNum = TableNum(kTables, kConnStatsTable); static constexpr uint32_t kHTTPTableNum = TableNum(kTables, kHTTPTable); @@ -111,6 +112,7 @@ class SocketTraceConnector : public BCCSourceConnector { static constexpr uint32_t kMuxTableNum = TableNum(kTables, kMuxTable); static constexpr uint32_t kAMQPTableNum = TableNum(kTables, kAMQPTable); static constexpr uint32_t kMongoDBTableNum = TableNum(kTables, kMongoDBTable); + static constexpr uint32_t kTLSTableNum = TableNum(kTables, kTLSTable); static constexpr auto kSamplingPeriod = std::chrono::milliseconds{200}; // TODO(yzhao): This is not used right now. Eventually use this to control data push frequency. diff --git a/src/stirling/source_connectors/socket_tracer/socket_trace_tables.h b/src/stirling/source_connectors/socket_tracer/socket_trace_tables.h index e37611213cd..20098738c31 100644 --- a/src/stirling/source_connectors/socket_tracer/socket_trace_tables.h +++ b/src/stirling/source_connectors/socket_tracer/socket_trace_tables.h @@ -32,3 +32,4 @@ #include "src/stirling/source_connectors/socket_tracer/nats_table.h" #include "src/stirling/source_connectors/socket_tracer/pgsql_table.h" #include "src/stirling/source_connectors/socket_tracer/redis_table.h" +#include "src/stirling/source_connectors/socket_tracer/tls_table.h" diff --git a/src/stirling/source_connectors/socket_tracer/testing/protocol_checkers.cc b/src/stirling/source_connectors/socket_tracer/testing/protocol_checkers.cc index 273e3dba17c..dc04bc9c330 100644 --- a/src/stirling/source_connectors/socket_tracer/testing/protocol_checkers.cc +++ b/src/stirling/source_connectors/socket_tracer/testing/protocol_checkers.cc @@ -19,6 +19,7 @@ #include "src/stirling/source_connectors/socket_tracer/testing/protocol_checkers.h" #include "src/stirling/source_connectors/socket_tracer/http_table.h" +#include "src/stirling/source_connectors/socket_tracer/tls_table.h" #include "src/stirling/testing/common.h" namespace px { @@ -28,6 +29,7 @@ namespace testing { namespace http = protocols::http; namespace mux = protocols::mux; namespace mongodb = protocols::mongodb; +namespace tls = protocols::tls; //----------------------------------------------------------------------------- // HTTP Checkers @@ -105,6 +107,20 @@ std::vector GetTargetRecords(const types::ColumnWrapperRecordBa return ToRecordVector(record_batch, target_record_indices); } +template <> +std::vector ToRecordVector(const types::ColumnWrapperRecordBatch& rb, + const std::vector& indices) { + std::vector result; + + for (const auto& idx : indices) { + tls::Record r; + r.req.body = rb[kTLSReqBodyIdx]->Get(idx); + r.resp.body = rb[kTLSRespBodyIdx]->Get(idx); + result.push_back(r); + } + return result; +} + } // namespace testing } // namespace stirling } // namespace px diff --git a/src/stirling/source_connectors/socket_tracer/testing/protocol_checkers.h b/src/stirling/source_connectors/socket_tracer/testing/protocol_checkers.h index 207eb68e89b..98eeb32d624 100644 --- a/src/stirling/source_connectors/socket_tracer/testing/protocol_checkers.h +++ b/src/stirling/source_connectors/socket_tracer/testing/protocol_checkers.h @@ -32,6 +32,7 @@ #include "src/stirling/source_connectors/socket_tracer/protocols/http/types.h" #include "src/stirling/source_connectors/socket_tracer/protocols/mongodb/types.h" #include "src/stirling/source_connectors/socket_tracer/protocols/mux/types.h" +#include "src/stirling/source_connectors/socket_tracer/protocols/tls/types.h" namespace px { namespace stirling { diff --git a/src/stirling/source_connectors/socket_tracer/tls_table.h b/src/stirling/source_connectors/socket_tracer/tls_table.h new file mode 100644 index 00000000000..1b0f3765fca --- /dev/null +++ b/src/stirling/source_connectors/socket_tracer/tls_table.h @@ -0,0 +1,69 @@ +/* + * Copyright 2018- The Pixie Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +#include "src/stirling/core/output.h" +#include "src/stirling/core/types.h" +#include "src/stirling/source_connectors/socket_tracer/canonical_types.h" +#include "src/stirling/source_connectors/socket_tracer/protocols/tls/types.h" + +namespace px { +namespace stirling { + +// clang-format off +static constexpr DataElement kTLSElements[] = { + canonical_data_elements::kTime, + canonical_data_elements::kUPID, + canonical_data_elements::kRemoteAddr, + canonical_data_elements::kRemotePort, + canonical_data_elements::kLocalAddr, + canonical_data_elements::kLocalPort, + canonical_data_elements::kTraceRole, + {"req_type", "The content type of the TLS record (e.g. handshake, alert, heartbeat, etc)", + types::DataType::INT64, + types::SemanticType::ST_NONE, + types::PatternType::GENERAL_ENUM}, + {"req_body", "Request body in JSON format. Structure depends on content type (e.g. handshakes contain TLS extensions, version negotiated, etc.)", + types::DataType::STRING, + types::SemanticType::ST_NONE, + types::PatternType::STRUCTURED}, + {"resp_body", "Response body in JSON format. Structure depends on content type (e.g. handshakes contain TLS extensions, version negotiated, etc.)", + types::DataType::STRING, + types::SemanticType::ST_NONE, + types::PatternType::STRUCTURED}, + canonical_data_elements::kLatencyNS, +#ifndef NDEBUG + canonical_data_elements::kPXInfo, +#endif +}; +// clang-format on + +static constexpr auto kTLSTable = + DataTableSchema("tls_events", "TLS request-response pair events", kTLSElements); +DEFINE_PRINT_TABLE(TLS) + +constexpr int kTLSUPIDIdx = kTLSTable.ColIndex("upid"); +constexpr int kTLSCmdIdx = kTLSTable.ColIndex("req_type"); +constexpr int kTLSReqBodyIdx = kTLSTable.ColIndex("req_body"); +constexpr int kTLSRespBodyIdx = kTLSTable.ColIndex("resp_body"); + +} // namespace stirling +} // namespace px diff --git a/src/stirling/source_connectors/socket_tracer/tls_trace_bpf_test.cc b/src/stirling/source_connectors/socket_tracer/tls_trace_bpf_test.cc new file mode 100644 index 00000000000..7931cc9f53e --- /dev/null +++ b/src/stirling/source_connectors/socket_tracer/tls_trace_bpf_test.cc @@ -0,0 +1,145 @@ +/* + * Copyright 2018- The Pixie Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include + +#include + +#include "src/common/base/base.h" +#include "src/common/exec/exec.h" +#include "src/common/testing/test_environment.h" +#include "src/shared/types/column_wrapper.h" +#include "src/shared/types/types.h" +#include "src/stirling/source_connectors/socket_tracer/socket_trace_connector.h" +#include "src/stirling/source_connectors/socket_tracer/testing/container_images/curl_container.h" +#include "src/stirling/source_connectors/socket_tracer/testing/container_images/nginx_openssl_3_0_8_container.h" +#include "src/stirling/source_connectors/socket_tracer/testing/protocol_checkers.h" +#include "src/stirling/source_connectors/socket_tracer/testing/socket_trace_bpf_test_fixture.h" +#include "src/stirling/testing/common.h" + +namespace px { +namespace stirling { + +namespace tls = protocols::tls; + +using ::px::stirling::testing::FindRecordIdxMatchesPID; +using ::px::stirling::testing::GetTargetRecords; +using ::px::stirling::testing::SocketTraceBPFTestFixture; +using ::px::stirling::testing::ToRecordVector; + +using ::testing::IsTrue; +using ::testing::SizeIs; +using ::testing::StrEq; +using ::testing::UnorderedElementsAre; + +class NginxOpenSSL_3_0_8_ContainerWrapper + : public ::px::stirling::testing::NginxOpenSSL_3_0_8_Container { + public: + int32_t PID() const { return NginxWorkerPID(); } +}; + +bool Init() { + // Make sure TLS tracing is enabled. + FLAGS_stirling_enable_tls_tracing = true; + + // We turn off CQL and NATS tracing to give some BPF instructions back for TLS. + // This is required for older kernels with only 4096 BPF instructions. + FLAGS_stirling_enable_cass_tracing = false; + FLAGS_stirling_enable_nats_tracing = false; + FLAGS_stirling_enable_amqp_tracing = false; + return true; +} + +//----------------------------------------------------------------------------- +// Test Scenarios +//----------------------------------------------------------------------------- + +tls::Record GetExpectedTLSRecord() { + tls::Record expected_record; + return expected_record; +} + +class TLSVersionParameterizedTest + : public SocketTraceBPFTestFixture, + public ::testing::WithParamInterface { + protected: + TLSVersionParameterizedTest() { + Init(); + + // Run the nginx HTTPS server. + // The container runner will make sure it is in the ready state before unblocking. + // Stirling will run after this unblocks, as part of SocketTraceBPFTest SetUp(). + constexpr bool kHostPid = false; + StatusOr run_result = server_.Run(std::chrono::seconds{60}, {}, {}, kHostPid); + PX_CHECK_OK(run_result); + + // Sleep an additional second, just to be safe. + sleep(1); + } + + void TestTLSVersion(const std::string& tls_version, const std::string& tls_max_version) { + FLAGS_stirling_conn_trace_pid = this->server_.PID(); + + this->StartTransferDataThread(); + + // Make an SSL request with curl. + ::px::stirling::testing::CurlContainer client; + constexpr bool kHostPid = false; + ASSERT_OK( + client.Run(std::chrono::seconds{60}, + {absl::Substitute("--network=container:$0", this->server_.container_name())}, + {"--insecure", "-s", "-S", "--resolve", "test-host:443:127.0.0.1", + absl::Substitute("--tlsv$0", tls_version), "--tls-max", tls_max_version, + "https://test-host/index.html"}, + kHostPid)); + client.Wait(); + this->StopTransferDataThread(); + + auto records = this->GetTraceRecords(this->server_.PID()); + EXPECT_THAT(records, SizeIs(1)); + EXPECT_GT(records[0].req.body.size(), 0); + auto sni_str = R"({"extensions":{"server_name":["test-host"]}})"; + EXPECT_THAT(records[0].req.body, StrEq(sni_str)); + } + + // Returns the trace records of the process specified by the input pid. + std::vector GetTraceRecords(int pid) { + std::vector tablets = + this->ConsumeRecords(SocketTraceConnector::kTLSTableNum); + if (tablets.empty()) { + return {}; + } + types::ColumnWrapperRecordBatch record_batch = tablets[0].records; + std::vector server_record_indices = + FindRecordIdxMatchesPID(record_batch, kTLSUPIDIdx, pid); + return ToRecordVector(record_batch, server_record_indices); + } + + NginxOpenSSL_3_0_8_ContainerWrapper server_; +}; + +INSTANTIATE_TEST_SUITE_P(TLSVersions, TLSVersionParameterizedTest, ::testing::Values("1.2")); + +TEST_P(TLSVersionParameterizedTest, TestTLSVersions) { + const std::string& tls_version = GetParam(); + TestTLSVersion(tls_version, tls_version); +} + +} // namespace stirling +} // namespace px