blob: 0c4f049ce1de6bb601d8c0a27561b02582c3abef [file] [log] [blame]
/*
* 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.
*/
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <vector>
#include "netdbpf/NetworkTraceHandler.h"
#include "protos/perfetto/config/android/network_trace_config.gen.h"
#include "protos/perfetto/trace/android/network_trace.pb.h"
#include "protos/perfetto/trace/trace.pb.h"
#include "protos/perfetto/trace/trace_packet.pb.h"
namespace android {
namespace bpf {
using ::perfetto::protos::NetworkPacketEvent;
using ::perfetto::protos::NetworkPacketTraceConfig;
using ::perfetto::protos::Trace;
using ::perfetto::protos::TracePacket;
using ::perfetto::protos::TrafficDirection;
class NetworkTraceHandlerTest : public testing::Test {
protected:
// Starts a tracing session with the handler under test.
std::unique_ptr<perfetto::TracingSession> StartTracing(
NetworkPacketTraceConfig settings) {
perfetto::TracingInitArgs args;
args.backends = perfetto::kInProcessBackend;
perfetto::Tracing::Initialize(args);
perfetto::DataSourceDescriptor dsd;
dsd.set_name("test.network_packets");
NetworkTraceHandler::Register(dsd, /*isTest=*/true);
perfetto::TraceConfig cfg;
cfg.add_buffers()->set_size_kb(1024);
auto* config = cfg.add_data_sources()->mutable_config();
config->set_name("test.network_packets");
config->set_network_packet_trace_config_raw(settings.SerializeAsString());
auto session = perfetto::Tracing::NewTrace(perfetto::kInProcessBackend);
session->Setup(cfg);
session->StartBlocking();
return session;
}
// Stops the trace session and reports all relevant trace packets.
bool StopTracing(perfetto::TracingSession* session,
std::vector<TracePacket>* output) {
session->StopBlocking();
Trace trace;
std::vector<char> raw_trace = session->ReadTraceBlocking();
if (!trace.ParseFromArray(raw_trace.data(), raw_trace.size())) {
ADD_FAILURE() << "trace.ParseFromArray failed";
return false;
}
// This is a real trace and includes irrelevant trace packets such as trace
// metadata. The following strips the results to just the packets we want.
for (const auto& pkt : trace.packet()) {
if (pkt.has_network_packet() || pkt.has_network_packet_bundle()) {
output->emplace_back(pkt);
}
}
return true;
}
// This runs a trace with a single call to Write.
bool TraceAndSortPackets(const std::vector<PacketTrace>& input,
std::vector<TracePacket>* output,
NetworkPacketTraceConfig config = {}) {
auto session = StartTracing(config);
NetworkTraceHandler::Trace([&](NetworkTraceHandler::TraceContext ctx) {
ctx.GetDataSourceLocked()->Write(input, ctx);
ctx.Flush();
});
if (!StopTracing(session.get(), output)) {
return false;
}
// Sort to provide deterministic ordering regardless of Perfetto internals
// or implementation-defined (e.g. hash map) reshuffling.
std::sort(output->begin(), output->end(),
[](const TracePacket& a, const TracePacket& b) {
return a.timestamp() < b.timestamp();
});
return true;
}
};
TEST_F(NetworkTraceHandlerTest, WriteBasicFields) {
std::vector<PacketTrace> input = {
PacketTrace{
.timestampNs = 1000,
.length = 100,
.uid = 10,
.tag = 123,
.ipProto = IPPROTO_TCP,
.tcpFlags = 1,
},
};
std::vector<TracePacket> events;
ASSERT_TRUE(TraceAndSortPackets(input, &events));
ASSERT_EQ(events.size(), 1);
EXPECT_THAT(events[0].timestamp(), 1000);
EXPECT_THAT(events[0].network_packet().uid(), 10);
EXPECT_THAT(events[0].network_packet().tag(), 123);
EXPECT_THAT(events[0].network_packet().ip_proto(), 6);
EXPECT_THAT(events[0].network_packet().tcp_flags(), 1);
EXPECT_THAT(events[0].network_packet().length(), 100);
EXPECT_THAT(events[0].has_sequence_flags(), false);
}
TEST_F(NetworkTraceHandlerTest, WriteDirectionAndPorts) {
std::vector<PacketTrace> input = {
PacketTrace{
.timestampNs = 1,
.sport = htons(8080),
.dport = htons(443),
.egress = true,
.ipProto = IPPROTO_TCP,
},
PacketTrace{
.timestampNs = 2,
.sport = htons(443),
.dport = htons(8080),
.egress = false,
.ipProto = IPPROTO_TCP,
},
};
std::vector<TracePacket> events;
ASSERT_TRUE(TraceAndSortPackets(input, &events));
ASSERT_EQ(events.size(), 2);
EXPECT_THAT(events[0].network_packet().local_port(), 8080);
EXPECT_THAT(events[0].network_packet().remote_port(), 443);
EXPECT_THAT(events[0].network_packet().direction(),
TrafficDirection::DIR_EGRESS);
EXPECT_THAT(events[1].network_packet().local_port(), 8080);
EXPECT_THAT(events[1].network_packet().remote_port(), 443);
EXPECT_THAT(events[1].network_packet().direction(),
TrafficDirection::DIR_INGRESS);
}
TEST_F(NetworkTraceHandlerTest, WriteIcmpTypeAndCode) {
std::vector<PacketTrace> input = {
PacketTrace{
.timestampNs = 1,
.sport = htons(11), // type
.dport = htons(22), // code
.egress = true,
.ipProto = IPPROTO_ICMP,
},
PacketTrace{
.timestampNs = 2,
.sport = htons(33), // type
.dport = htons(44), // code
.egress = false,
.ipProto = IPPROTO_ICMPV6,
},
};
std::vector<TracePacket> events;
ASSERT_TRUE(TraceAndSortPackets(input, &events));
ASSERT_EQ(events.size(), 2);
EXPECT_FALSE(events[0].network_packet().has_local_port());
EXPECT_FALSE(events[0].network_packet().has_remote_port());
EXPECT_THAT(events[0].network_packet().icmp_type(), 11);
EXPECT_THAT(events[0].network_packet().icmp_code(), 22);
EXPECT_THAT(events[0].network_packet().direction(),
TrafficDirection::DIR_EGRESS);
EXPECT_FALSE(events[1].network_packet().local_port());
EXPECT_FALSE(events[1].network_packet().remote_port());
EXPECT_THAT(events[1].network_packet().icmp_type(), 33);
EXPECT_THAT(events[1].network_packet().icmp_code(), 44);
EXPECT_THAT(events[1].network_packet().direction(),
TrafficDirection::DIR_INGRESS);
}
TEST_F(NetworkTraceHandlerTest, BasicBundling) {
// TODO: remove this once bundling becomes default. Until then, set arbitrary
// aggregation threshold to enable bundling.
NetworkPacketTraceConfig config;
config.set_aggregation_threshold(10);
std::vector<PacketTrace> input = {
PacketTrace{.timestampNs = 2, .length = 200, .uid = 123},
PacketTrace{.timestampNs = 1, .length = 100, .uid = 123},
PacketTrace{.timestampNs = 4, .length = 300, .uid = 123},
PacketTrace{.timestampNs = 2, .length = 400, .uid = 456},
PacketTrace{.timestampNs = 4, .length = 100, .uid = 456},
};
std::vector<TracePacket> events;
ASSERT_TRUE(TraceAndSortPackets(input, &events, config));
ASSERT_EQ(events.size(), 2);
EXPECT_THAT(events[0].timestamp(), 1);
EXPECT_THAT(events[0].network_packet_bundle().ctx().uid(), 123);
EXPECT_THAT(events[0].network_packet_bundle().packet_lengths(),
testing::ElementsAre(200, 100, 300));
EXPECT_THAT(events[0].network_packet_bundle().packet_timestamps(),
testing::ElementsAre(1, 0, 3));
EXPECT_THAT(events[1].timestamp(), 2);
EXPECT_THAT(events[1].network_packet_bundle().ctx().uid(), 456);
EXPECT_THAT(events[1].network_packet_bundle().packet_lengths(),
testing::ElementsAre(400, 100));
EXPECT_THAT(events[1].network_packet_bundle().packet_timestamps(),
testing::ElementsAre(0, 2));
}
TEST_F(NetworkTraceHandlerTest, AggregationThreshold) {
// With an aggregation threshold of 3, the set of packets with uid=123 will
// be aggregated (3>=3) whereas packets with uid=456 get per-packet info.
NetworkPacketTraceConfig config;
config.set_aggregation_threshold(3);
std::vector<PacketTrace> input = {
PacketTrace{.timestampNs = 2, .length = 200, .uid = 123},
PacketTrace{.timestampNs = 1, .length = 100, .uid = 123},
PacketTrace{.timestampNs = 4, .length = 300, .uid = 123},
PacketTrace{.timestampNs = 2, .length = 400, .uid = 456},
PacketTrace{.timestampNs = 4, .length = 100, .uid = 456},
};
std::vector<TracePacket> events;
ASSERT_TRUE(TraceAndSortPackets(input, &events, config));
ASSERT_EQ(events.size(), 2);
EXPECT_EQ(events[0].timestamp(), 1);
EXPECT_EQ(events[0].network_packet_bundle().ctx().uid(), 123);
EXPECT_EQ(events[0].network_packet_bundle().total_duration(), 3);
EXPECT_EQ(events[0].network_packet_bundle().total_packets(), 3);
EXPECT_EQ(events[0].network_packet_bundle().total_length(), 600);
EXPECT_EQ(events[1].timestamp(), 2);
EXPECT_EQ(events[1].network_packet_bundle().ctx().uid(), 456);
EXPECT_THAT(events[1].network_packet_bundle().packet_lengths(),
testing::ElementsAre(400, 100));
EXPECT_THAT(events[1].network_packet_bundle().packet_timestamps(),
testing::ElementsAre(0, 2));
}
TEST_F(NetworkTraceHandlerTest, DropLocalPort) {
NetworkPacketTraceConfig config;
config.set_drop_local_port(true);
config.set_aggregation_threshold(10);
__be16 a = htons(10000);
__be16 b = htons(10001);
std::vector<PacketTrace> input = {
// Recall that local is `src` for egress and `dst` for ingress.
PacketTrace{.timestampNs = 1, .length = 2, .sport = a, .egress = true},
PacketTrace{.timestampNs = 2, .length = 4, .dport = a, .egress = false},
PacketTrace{.timestampNs = 3, .length = 6, .sport = b, .egress = true},
PacketTrace{.timestampNs = 4, .length = 8, .dport = b, .egress = false},
};
// Set common fields.
for (PacketTrace& pkt : input) {
pkt.ipProto = IPPROTO_TCP;
}
std::vector<TracePacket> events;
ASSERT_TRUE(TraceAndSortPackets(input, &events, config));
ASSERT_EQ(events.size(), 2);
// Despite having different local ports, drop and bundle by remaining fields.
EXPECT_EQ(events[0].network_packet_bundle().ctx().direction(),
TrafficDirection::DIR_EGRESS);
EXPECT_THAT(events[0].network_packet_bundle().packet_lengths(),
testing::ElementsAre(2, 6));
EXPECT_EQ(events[1].network_packet_bundle().ctx().direction(),
TrafficDirection::DIR_INGRESS);
EXPECT_THAT(events[1].network_packet_bundle().packet_lengths(),
testing::ElementsAre(4, 8));
// Local port shouldn't be in output.
EXPECT_FALSE(events[0].network_packet_bundle().ctx().has_local_port());
EXPECT_FALSE(events[1].network_packet_bundle().ctx().has_local_port());
}
TEST_F(NetworkTraceHandlerTest, DropRemotePort) {
NetworkPacketTraceConfig config;
config.set_drop_remote_port(true);
config.set_aggregation_threshold(10);
__be16 a = htons(443);
__be16 b = htons(80);
std::vector<PacketTrace> input = {
// Recall that remote is `dst` for egress and `src` for ingress.
PacketTrace{.timestampNs = 1, .length = 2, .dport = a, .egress = true},
PacketTrace{.timestampNs = 2, .length = 4, .sport = a, .egress = false},
PacketTrace{.timestampNs = 3, .length = 6, .dport = b, .egress = true},
PacketTrace{.timestampNs = 4, .length = 8, .sport = b, .egress = false},
};
// Set common fields.
for (PacketTrace& pkt : input) {
pkt.ipProto = IPPROTO_TCP;
}
std::vector<TracePacket> events;
ASSERT_TRUE(TraceAndSortPackets(input, &events, config));
ASSERT_EQ(events.size(), 2);
// Despite having different remote ports, drop and bundle by remaining fields.
EXPECT_EQ(events[0].network_packet_bundle().ctx().direction(),
TrafficDirection::DIR_EGRESS);
EXPECT_THAT(events[0].network_packet_bundle().packet_lengths(),
testing::ElementsAre(2, 6));
EXPECT_EQ(events[1].network_packet_bundle().ctx().direction(),
TrafficDirection::DIR_INGRESS);
EXPECT_THAT(events[1].network_packet_bundle().packet_lengths(),
testing::ElementsAre(4, 8));
// Remote port shouldn't be in output.
EXPECT_FALSE(events[0].network_packet_bundle().ctx().has_remote_port());
EXPECT_FALSE(events[1].network_packet_bundle().ctx().has_remote_port());
}
TEST_F(NetworkTraceHandlerTest, DropTcpFlags) {
NetworkPacketTraceConfig config;
config.set_drop_tcp_flags(true);
config.set_aggregation_threshold(10);
std::vector<PacketTrace> input = {
PacketTrace{.timestampNs = 1, .length = 1, .uid = 123, .tcpFlags = 1},
PacketTrace{.timestampNs = 2, .length = 2, .uid = 123, .tcpFlags = 2},
PacketTrace{.timestampNs = 3, .length = 3, .uid = 456, .tcpFlags = 1},
PacketTrace{.timestampNs = 4, .length = 4, .uid = 456, .tcpFlags = 2},
};
// Set common fields.
for (PacketTrace& pkt : input) {
pkt.ipProto = IPPROTO_TCP;
}
std::vector<TracePacket> events;
ASSERT_TRUE(TraceAndSortPackets(input, &events, config));
ASSERT_EQ(events.size(), 2);
// Despite having different tcp flags, drop and bundle by remaining fields.
EXPECT_EQ(events[0].network_packet_bundle().ctx().uid(), 123);
EXPECT_THAT(events[0].network_packet_bundle().packet_lengths(),
testing::ElementsAre(1, 2));
EXPECT_EQ(events[1].network_packet_bundle().ctx().uid(), 456);
EXPECT_THAT(events[1].network_packet_bundle().packet_lengths(),
testing::ElementsAre(3, 4));
// Tcp flags shouldn't be in output.
EXPECT_FALSE(events[0].network_packet_bundle().ctx().has_tcp_flags());
EXPECT_FALSE(events[1].network_packet_bundle().ctx().has_tcp_flags());
}
TEST_F(NetworkTraceHandlerTest, Interning) {
NetworkPacketTraceConfig config;
config.set_intern_limit(2);
// The test writes 4 packets coming from three sources (uids). With an intern
// limit of 2, the first two sources should be interned. This test splits this
// into individual writes since internally an unordered map is used and would
// otherwise non-deterministically choose what to intern (this is fine for
// real use, but not good for test assertions).
std::vector<std::vector<PacketTrace>> inputs = {
{PacketTrace{.timestampNs = 1, .uid = 123}},
{PacketTrace{.timestampNs = 2, .uid = 456}},
{PacketTrace{.timestampNs = 3, .uid = 789}},
{PacketTrace{.timestampNs = 4, .uid = 123}},
};
auto session = StartTracing(config);
NetworkTraceHandler::Trace([&](NetworkTraceHandler::TraceContext ctx) {
ctx.GetDataSourceLocked()->Write(inputs[0], ctx);
ctx.GetDataSourceLocked()->Write(inputs[1], ctx);
ctx.GetDataSourceLocked()->Write(inputs[2], ctx);
ctx.GetDataSourceLocked()->Write(inputs[3], ctx);
ctx.Flush();
});
std::vector<TracePacket> events;
ASSERT_TRUE(StopTracing(session.get(), &events));
ASSERT_EQ(events.size(), 4);
// First time seen, emit new interned data, bundle uses iid instead of ctx.
EXPECT_EQ(events[0].network_packet_bundle().iid(), 1);
ASSERT_EQ(events[0].interned_data().packet_context().size(), 1);
EXPECT_EQ(events[0].interned_data().packet_context(0).iid(), 1);
EXPECT_EQ(events[0].interned_data().packet_context(0).ctx().uid(), 123);
EXPECT_EQ(events[0].sequence_flags(),
TracePacket::SEQ_INCREMENTAL_STATE_CLEARED);
// First time seen, emit new interned data, bundle uses iid instead of ctx.
EXPECT_EQ(events[1].network_packet_bundle().iid(), 2);
ASSERT_EQ(events[1].interned_data().packet_context().size(), 1);
EXPECT_EQ(events[1].interned_data().packet_context(0).iid(), 2);
EXPECT_EQ(events[1].interned_data().packet_context(0).ctx().uid(), 456);
EXPECT_EQ(events[1].sequence_flags(),
TracePacket::SEQ_NEEDS_INCREMENTAL_STATE);
// Not enough room in intern table (limit 2), inline the context.
EXPECT_EQ(events[2].network_packet_bundle().ctx().uid(), 789);
EXPECT_EQ(events[2].interned_data().packet_context().size(), 0);
EXPECT_EQ(events[2].sequence_flags(),
TracePacket::SEQ_NEEDS_INCREMENTAL_STATE);
// Second time seen, no need to re-emit interned data, only record iid.
EXPECT_EQ(events[3].network_packet_bundle().iid(), 1);
EXPECT_EQ(events[3].interned_data().packet_context().size(), 0);
EXPECT_EQ(events[3].sequence_flags(),
TracePacket::SEQ_NEEDS_INCREMENTAL_STATE);
}
} // namespace bpf
} // namespace android