/* * Copyright (C) 2023 The Android Open Source Project * * 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. */ #define LOG_TAG "NetworkTrace" #include "netdbpf/NetworkTraceHandler.h" #include #include #include #include #include #include #include #include #include // Note: this is initializing state for a templated Perfetto type that resides // in the `perfetto` namespace. This must be defined in the global scope. PERFETTO_DEFINE_DATA_SOURCE_STATIC_MEMBERS(android::bpf::NetworkTraceHandler); namespace android { namespace bpf { using ::android::bpf::internal::NetworkTracePoller; using ::perfetto::protos::pbzero::NetworkPacketBundle; using ::perfetto::protos::pbzero::NetworkPacketEvent; using ::perfetto::protos::pbzero::NetworkPacketTraceConfig; using ::perfetto::protos::pbzero::TracePacket; using ::perfetto::protos::pbzero::TrafficDirection; // Bundling takes groups of packets with similar contextual fields (generally, // all fields except timestamp and length) and summarises them in a single trace // packet. For example, rather than // // {.timestampNs = 1, .uid = 1000, .tag = 123, .len = 72} // {.timestampNs = 2, .uid = 1000, .tag = 123, .len = 100} // {.timestampNs = 5, .uid = 1000, .tag = 123, .len = 456} // // The output will be something like // { // .timestamp = 1 // .ctx = {.uid = 1000, .tag = 123} // .timestamp = [0, 1, 4], // delta encoded // .length = [72, 100, 456], // should be zipped with timestamps // } // // Most workloads have many packets from few contexts. Bundling greatly reduces // the amount of redundant information written, thus reducing the overall trace // size. Interning ids are similarly based on unique bundle contexts. // Based on boost::hash_combine template void HashCombine(std::size_t& seed, const T& val, const Rest&... rest) { seed ^= std::hash()(val) + 0x9e3779b9 + (seed << 6) + (seed >> 2); (HashCombine(seed, rest), ...); } // Details summarises the timestamp and lengths of packets in a bundle. struct BundleDetails { std::vector> time_and_len; uint64_t minTs = std::numeric_limits::max(); uint64_t maxTs = std::numeric_limits::min(); uint32_t bytes = 0; }; BundleKey::BundleKey(const PacketTrace& pkt) : ifindex(pkt.ifindex), uid(pkt.uid), tag(pkt.tag), egress(pkt.egress), ipProto(pkt.ipProto), ipVersion(pkt.ipVersion) { switch (ipProto) { case IPPROTO_TCP: tcpFlags = pkt.tcpFlags; FALLTHROUGH_INTENDED; case IPPROTO_DCCP: case IPPROTO_UDP: case IPPROTO_UDPLITE: case IPPROTO_SCTP: localPort = ntohs(pkt.egress ? pkt.sport : pkt.dport); remotePort = ntohs(pkt.egress ? pkt.dport : pkt.sport); break; case IPPROTO_ICMP: case IPPROTO_ICMPV6: icmpType = ntohs(pkt.sport); icmpCode = ntohs(pkt.dport); break; } } #define AGG_FIELDS(x) \ (x).ifindex, (x).uid, (x).tag, (x).egress, (x).ipProto, (x).ipVersion, \ (x).tcpFlags, (x).localPort, (x).remotePort, (x).icmpType, (x).icmpCode std::size_t BundleHash::operator()(const BundleKey& a) const { std::size_t seed = 0; HashCombine(seed, AGG_FIELDS(a)); return seed; } bool BundleEq::operator()(const BundleKey& a, const BundleKey& b) const { return std::tie(AGG_FIELDS(a)) == std::tie(AGG_FIELDS(b)); } // static void NetworkTraceHandler::RegisterDataSource() { ALOGD("Registering Perfetto data source"); perfetto::DataSourceDescriptor dsd; dsd.set_name("android.network_packets"); NetworkTraceHandler::Register(dsd); } // static void NetworkTraceHandler::InitPerfettoTracing() { perfetto::TracingInitArgs args = {}; args.backends |= perfetto::kSystemBackend; // The following line disables the Perfetto system consumer. Perfetto inlines // the call to `Initialize` which allows the compiler to see that the branch // with the SystemConsumerTracingBackend is not used. With LTO enabled, this // strips the Perfetto consumer code and reduces the size of this binary by // around 270KB total. Be careful when changing this value. args.enable_system_consumer = false; perfetto::Tracing::Initialize(args); NetworkTraceHandler::RegisterDataSource(); } // static NetworkTracePoller NetworkTraceHandler::sPoller( [](const std::vector& packets) { // Trace calls the provided callback for each active session. The context // gets a reference to the NetworkTraceHandler instance associated with // the session and delegates writing. The corresponding handler will write // with the setting specified in the trace config. NetworkTraceHandler::Trace([&](NetworkTraceHandler::TraceContext ctx) { perfetto::LockedHandle handle = ctx.GetDataSourceLocked(); // The underlying handle can be invalidated between when Trace starts // and GetDataSourceLocked is called, but not while the LockedHandle // exists and holds the lock. Check validity prior to use. if (handle.valid()) { handle->Write(packets, ctx); } }); }); void NetworkTraceHandler::OnSetup(const SetupArgs& args) { const std::string& raw = args.config->network_packet_trace_config_raw(); NetworkPacketTraceConfig::Decoder config(raw); mPollMs = config.poll_ms(); if (mPollMs < 100) { ALOGI("poll_ms is missing or below the 100ms minimum. Increasing to 100ms"); mPollMs = 100; } mInternLimit = config.intern_limit(); mAggregationThreshold = config.aggregation_threshold(); mDropLocalPort = config.drop_local_port(); mDropRemotePort = config.drop_remote_port(); mDropTcpFlags = config.drop_tcp_flags(); } void NetworkTraceHandler::OnStart(const StartArgs&) { if (mIsTest) return; // Don't touch non-hermetic bpf in test. mStarted = sPoller.Start(mPollMs); } void NetworkTraceHandler::OnStop(const StopArgs&) { if (mIsTest) return; // Don't touch non-hermetic bpf in test. if (mStarted) sPoller.Stop(); mStarted = false; // Although this shouldn't be required, there seems to be some cases when we // don't fill enough of a Perfetto Chunk for Perfetto to automatically commit // the traced data. This manually flushes OnStop so we commit at least once. NetworkTraceHandler::Trace([&](NetworkTraceHandler::TraceContext ctx) { perfetto::LockedHandle handle = ctx.GetDataSourceLocked(); // Trace is called for all active handlers, only flush our context. Since // handle doesn't have a `.get()`, use `*` and `&` to get what it points to. if (&(*handle) != this) return; ctx.Flush(); }); } void NetworkTraceHandler::Write(const std::vector& packets, NetworkTraceHandler::TraceContext& ctx) { // TODO: remove this fallback once Perfetto stable has support for bundles. if (!mInternLimit && !mAggregationThreshold) { for (const PacketTrace& pkt : packets) { auto dst = ctx.NewTracePacket(); dst->set_timestamp(pkt.timestampNs); auto* event = dst->set_network_packet(); event->set_length(pkt.length); Fill(BundleKey(pkt), event); } return; } uint64_t minTs = std::numeric_limits::max(); std::unordered_map bundles; for (const PacketTrace& pkt : packets) { BundleKey key(pkt); // Dropping fields should remove them from the output and remove them from // the aggregation key. Reset the optionals to indicate omission. if (mDropTcpFlags) key.tcpFlags.reset(); if (mDropLocalPort) key.localPort.reset(); if (mDropRemotePort) key.remotePort.reset(); minTs = std::min(minTs, pkt.timestampNs); BundleDetails& bundle = bundles[key]; bundle.time_and_len.emplace_back(pkt.timestampNs, pkt.length); bundle.minTs = std::min(bundle.minTs, pkt.timestampNs); bundle.maxTs = std::max(bundle.maxTs, pkt.timestampNs); bundle.bytes += pkt.length; } NetworkTraceState* incr_state = ctx.GetIncrementalState(); for (const auto& kv : bundles) { const BundleKey& key = kv.first; const BundleDetails& details = kv.second; auto dst = ctx.NewTracePacket(); dst->set_timestamp(details.minTs); // Incremental state is only used when interning. Set the flag based on // whether state was cleared. Leave the flag empty in non-intern configs. if (mInternLimit > 0) { if (incr_state->cleared) { dst->set_sequence_flags(TracePacket::SEQ_INCREMENTAL_STATE_CLEARED); incr_state->cleared = false; } else { dst->set_sequence_flags(TracePacket::SEQ_NEEDS_INCREMENTAL_STATE); } } auto* event = FillWithInterning(incr_state, key, dst.get()); int count = details.time_and_len.size(); if (!mAggregationThreshold || count < mAggregationThreshold) { protozero::PackedVarInt offsets; protozero::PackedVarInt lengths; for (const auto& kv : details.time_and_len) { offsets.Append(kv.first - details.minTs); lengths.Append(kv.second); } event->set_packet_timestamps(offsets); event->set_packet_lengths(lengths); } else { event->set_total_duration(details.maxTs - details.minTs); event->set_total_length(details.bytes); event->set_total_packets(count); } } } void NetworkTraceHandler::Fill(const BundleKey& src, NetworkPacketEvent* event) { event->set_direction(src.egress ? TrafficDirection::DIR_EGRESS : TrafficDirection::DIR_INGRESS); event->set_uid(src.uid); event->set_tag(src.tag); if (src.tcpFlags.has_value()) event->set_tcp_flags(*src.tcpFlags); if (src.localPort.has_value()) event->set_local_port(*src.localPort); if (src.remotePort.has_value()) event->set_remote_port(*src.remotePort); if (src.icmpType.has_value()) event->set_icmp_type(*src.icmpType); if (src.icmpCode.has_value()) event->set_icmp_code(*src.icmpCode); event->set_ip_proto(src.ipProto); char ifname[IF_NAMESIZE] = {}; if (if_indextoname(src.ifindex, ifname) == ifname) { event->set_interface(std::string(ifname)); } else { event->set_interface("error"); } } NetworkPacketBundle* NetworkTraceHandler::FillWithInterning( NetworkTraceState* state, const BundleKey& key, TracePacket* dst) { uint64_t iid = 0; bool found = false; if (state->iids.size() < mInternLimit) { auto [iter, success] = state->iids.try_emplace(key, state->iids.size() + 1); iid = iter->second; found = true; if (success) { // If we successfully empaced, record the newly interned data. auto* packet_context = dst->set_interned_data()->add_packet_context(); Fill(key, packet_context->set_ctx()); packet_context->set_iid(iid); } } else { auto iter = state->iids.find(key); if (iter != state->iids.end()) { iid = iter->second; found = true; } } auto* event = dst->set_network_packet_bundle(); if (found) { event->set_iid(iid); } else { Fill(key, event->set_ctx()); } return event; } } // namespace bpf } // namespace android