|
| 1 | +// Copyright 2025 Google LLC |
| 2 | +// |
| 3 | +// Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 | +// you may not use this file except in compliance with the License. |
| 5 | +// You may obtain a copy of the License at |
| 6 | +// |
| 7 | +// http://www.apache.org/licenses/LICENSE-2.0 |
| 8 | +// |
| 9 | +// Unless required by applicable law or agreed to in writing, software |
| 10 | +// distributed under the License is distributed on an "AS IS" BASIS, |
| 11 | +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 | +// See the License for the specific language governing permissions and |
| 13 | +// limitations under the License. |
| 14 | + |
| 15 | +#include "tests/packet_capture/packet_capture_test.h" |
| 16 | + |
| 17 | +#include <cstdint> |
| 18 | +#include <memory> |
| 19 | +#include <optional> |
| 20 | +#include <string> |
| 21 | +#include <vector> |
| 22 | + |
| 23 | +#include "absl/container/flat_hash_map.h" |
| 24 | +#include "absl/flags/declare.h" |
| 25 | +#include "absl/status/statusor.h" |
| 26 | +#include "absl/strings/numbers.h" |
| 27 | +#include "absl/strings/str_cat.h" |
| 28 | +#include "absl/strings/string_view.h" |
| 29 | +#include "absl/time/clock.h" |
| 30 | +#include "absl/time/time.h" |
| 31 | +#include "absl/types/optional.h" |
| 32 | +#include "glog/logging.h" |
| 33 | +#include "gutil/collections.h" |
| 34 | +#include "gutil/status.h" |
| 35 | +#include "lib/gnmi/gnmi_helper.h" |
| 36 | +#include "lib/gnmi/openconfig.pb.h" |
| 37 | +#include "p4/config/v1/p4info.pb.h" |
| 38 | +#include "p4/v1/p4runtime.pb.h" |
| 39 | +#include "p4_pdpi/ir.h" |
| 40 | +#include "p4_pdpi/ir.pb.h" |
| 41 | +#include "p4_pdpi/p4_runtime_session.h" |
| 42 | +#include "p4_pdpi/p4_runtime_session_extras.h" |
| 43 | +#include "p4_pdpi/packetlib/packetlib.h" |
| 44 | +#include "p4_pdpi/packetlib/packetlib.pb.h" |
| 45 | +#include "p4_pdpi/string_encodings/hex_string.h" |
| 46 | +#include "proto/gnmi/gnmi.pb.h" |
| 47 | +#include "sai_p4/instantiations/google/instantiations.h" |
| 48 | +#include "sai_p4/instantiations/google/sai_pd.pb.h" |
| 49 | +#include "sai_p4/instantiations/google/test_tools/test_entries.h" |
| 50 | +#include "tests/forwarding/util.h" |
| 51 | +#include "tests/lib/switch_test_setup_helpers.h" |
| 52 | +#include "tests/packet_capture/packet_capture_test_util.h" |
| 53 | +#include "thinkit/mirror_testbed.h" |
| 54 | +#include "thinkit/proto/generic_testbed.pb.h" |
| 55 | +#include "thinkit/switch.h" |
| 56 | +#include "gmock/gmock.h" |
| 57 | +#include "gtest/gtest.h" |
| 58 | + |
| 59 | +ABSL_DECLARE_FLAG(std::optional<sai::Instantiation>, switch_instantiation); |
| 60 | + |
| 61 | +namespace pins_test { |
| 62 | +namespace { |
| 63 | + |
| 64 | +using ::p4::config::v1::P4Info; |
| 65 | +using pctutil::SutToControlLink; |
| 66 | + |
| 67 | +// Returns a set of table entries that will cause a switch to mirror all packets |
| 68 | +// on an incoming port to a mirror-to-port using PSAMP encapsulation and |
| 69 | +// adding a specified Vlan tag. |
| 70 | +absl::StatusOr<std::vector<p4::v1::Entity>> |
| 71 | +ConstructEntriesToMirrorTrafficWithVlanTag( |
| 72 | + const pdpi::IrP4Info &ir_p4info, const std::string &p4rt_src_port_id, |
| 73 | + const sai::MirrorSessionParams &mirror_session) { |
| 74 | + ASSIGN_OR_RETURN( |
| 75 | + std::vector<p4::v1::Entity> pi_entities, |
| 76 | + sai::EntryBuilder() |
| 77 | + .AddDisableVlanChecksEntry() |
| 78 | + .AddMirrorSessionTableEntry(mirror_session) |
| 79 | + .AddMarkToMirrorAclEntry(sai::MarkToMirrorParams{ |
| 80 | + .ingress_port = p4rt_src_port_id, |
| 81 | + .mirror_session_id = mirror_session.mirror_session_id, |
| 82 | + }) |
| 83 | + .GetDedupedPiEntities(ir_p4info)); |
| 84 | + |
| 85 | + return pi_entities; |
| 86 | +} |
| 87 | + |
| 88 | +TEST_P(PacketCaptureTestWithoutIxia, PsampEncapsulatedMirroringTest) { |
| 89 | + LOG(INFO) << "-- START OF TEST ---------------------------------------------"; |
| 90 | + Testbed().Environment().SetTestCaseID("TBD"); |
| 91 | + |
| 92 | + // Setup: the testbed consists of a SUT connected to a control device |
| 93 | + // that allows us to send and receive packets to/from the SUT. |
| 94 | + thinkit::Switch &sut = Testbed().Sut(); |
| 95 | + thinkit::Switch &control_device = Testbed().ControlSwitch(); |
| 96 | + |
| 97 | + // Configure mirror testbed. |
| 98 | + std::unique_ptr<pdpi::P4RuntimeSession> sut_p4rt_session, |
| 99 | + control_p4rt_session; |
| 100 | + ASSERT_OK_AND_ASSIGN(sut_p4rt_session, |
| 101 | + pins_test::ConfigureSwitchAndReturnP4RuntimeSession( |
| 102 | + sut, std::nullopt, std::nullopt)); |
| 103 | + |
| 104 | + ASSERT_OK_AND_ASSIGN(control_p4rt_session, |
| 105 | + pins_test::ConfigureSwitchAndReturnP4RuntimeSession( |
| 106 | + control_device, std::nullopt, std::nullopt)); |
| 107 | + |
| 108 | + ASSERT_OK_AND_ASSIGN(const P4Info &p4info, |
| 109 | + pdpi::GetP4Info(*sut_p4rt_session)); |
| 110 | + ASSERT_OK_AND_ASSIGN(const P4Info &control_p4info, |
| 111 | + pdpi::GetP4Info(*control_p4rt_session)); |
| 112 | + ASSERT_OK_AND_ASSIGN(const pdpi::IrP4Info ir_p4info, |
| 113 | + pdpi::CreateIrP4Info(p4info)); |
| 114 | + ASSERT_OK_AND_ASSIGN(const pdpi::IrP4Info ir_control_p4info, |
| 115 | + pdpi::CreateIrP4Info(control_p4info)); |
| 116 | + // Store P4Info for debugging purposes. |
| 117 | + EXPECT_OK( |
| 118 | + Testbed().Environment().StoreTestArtifact("p4info.textproto", p4info)); |
| 119 | + // Store gNMI config for debugging purposes. |
| 120 | + ASSERT_OK_AND_ASSIGN(auto sut_gnmi_stub, sut.CreateGnmiStub()); |
| 121 | + ASSERT_OK_AND_ASSIGN(std::string sut_gnmi_config, |
| 122 | + pins_test::GetGnmiConfig(*sut_gnmi_stub)); |
| 123 | + EXPECT_OK(Testbed().Environment().StoreTestArtifact("sut_gnmi_config.json", |
| 124 | + sut_gnmi_config)); |
| 125 | + |
| 126 | + ASSERT_OK_AND_ASSIGN(std::vector<p4::v1::Entity> pi_entities, |
| 127 | + sai::EntryBuilder() |
| 128 | + .AddEntryPuntingAllPackets(sai::PuntAction::kCopy) |
| 129 | + .GetDedupedPiEntities(ir_control_p4info)); |
| 130 | + |
| 131 | + ASSERT_OK(pdpi::InstallPiEntities(control_p4rt_session.get(), |
| 132 | + ir_control_p4info, pi_entities)); |
| 133 | + |
| 134 | + // Pick links to be used for packet injection and mirroring. |
| 135 | + ASSERT_OK_AND_ASSIGN(SutToControlLink link_used_for_test_packets, |
| 136 | + pctutil::PickSutToControlDeviceLinkThatsUp(Testbed())); |
| 137 | + LOG(INFO) << "Link used to inject test packets: " |
| 138 | + << link_used_for_test_packets; |
| 139 | + |
| 140 | + // Get P4RT IDs for SUT ports. |
| 141 | + absl::flat_hash_map<std::string, std::string> p4rt_id_by_interface; |
| 142 | + ASSERT_OK_AND_ASSIGN(p4rt_id_by_interface, |
| 143 | + GetAllInterfaceNameToPortId(*sut_gnmi_stub)); |
| 144 | + ASSERT_OK_AND_ASSIGN( |
| 145 | + const std::string kSutIngressPortP4rtId, |
| 146 | + gutil::FindOrStatus( |
| 147 | + p4rt_id_by_interface, |
| 148 | + link_used_for_test_packets.sut_ingress_port_gnmi_name)); |
| 149 | + ASSERT_OK_AND_ASSIGN( |
| 150 | + const std::string kSutEgressPortP4rtId, |
| 151 | + gutil::FindOrStatus(p4rt_id_by_interface, |
| 152 | + link_used_for_test_packets.sut_mtp_port_gnmi_name)); |
| 153 | + // Get P4RT IDs for Control Switch ports. |
| 154 | + ASSERT_OK_AND_ASSIGN(auto control_gnmi_stub, control_device.CreateGnmiStub()); |
| 155 | + ASSERT_OK_AND_ASSIGN(p4rt_id_by_interface, |
| 156 | + GetAllInterfaceNameToPortId(*control_gnmi_stub)); |
| 157 | + ASSERT_OK_AND_ASSIGN( |
| 158 | + const std::string kControlSwitchInjectPortP4rtId, |
| 159 | + gutil::FindOrStatus( |
| 160 | + p4rt_id_by_interface, |
| 161 | + link_used_for_test_packets.control_switch_inject_port_gnmi_name)); |
| 162 | + |
| 163 | + // Configure mirror session attributes. |
| 164 | + auto mirror_session_params = sai::MirrorSessionParams{ |
| 165 | + .mirror_session_id = "psamp_mirror", |
| 166 | + .monitor_port = kSutEgressPortP4rtId, |
| 167 | + .mirror_encap_src_mac = "00:00:00:22:22:22", |
| 168 | + .mirror_encap_dst_mac = "00:00:00:44:44:44", |
| 169 | + .mirror_encap_vlan_id = "0x0fe", |
| 170 | + .mirror_encap_src_ip = "2222:2222:2222:2222:2222:2222:2222:2222", |
| 171 | + .mirror_encap_dst_ip = "4444:4444:4444:4444:4444:4444:4444:4444", |
| 172 | + .mirror_encap_udp_src_port = "0x08ae", |
| 173 | + .mirror_encap_udp_dst_port = "0x1283"}; |
| 174 | + // Install ACL table entry to match on inject port on Control switch |
| 175 | + // and mirror-to-port on SUT. |
| 176 | + ASSERT_OK_AND_ASSIGN(auto entries, ConstructEntriesToMirrorTrafficWithVlanTag( |
| 177 | + ir_p4info, kSutIngressPortP4rtId, |
| 178 | + mirror_session_params)); |
| 179 | + ASSERT_OK( |
| 180 | + pdpi::InstallPiEntities(sut_p4rt_session.get(), ir_p4info, entries)); |
| 181 | + |
| 182 | + LOG(INFO) << "injecting test packet: " |
| 183 | + << GetParam().test_packet.DebugString(); |
| 184 | + ASSERT_OK_AND_ASSIGN(std::string raw_packet, |
| 185 | + packetlib::SerializePacket(GetParam().test_packet)); |
| 186 | + |
| 187 | + // Read ingress and egress port stat counters before packet injection. |
| 188 | + ASSERT_OK_AND_ASSIGN( |
| 189 | + uint64_t out_packets_pre, |
| 190 | + pctutil::GetGnmiStat("out-unicast-pkts", |
| 191 | + link_used_for_test_packets.sut_mtp_port_gnmi_name, |
| 192 | + sut_gnmi_stub.get())); |
| 193 | + ASSERT_OK_AND_ASSIGN( |
| 194 | + uint64_t in_packets_pre, |
| 195 | + pctutil::GetGnmiStat( |
| 196 | + "in-unicast-pkts", |
| 197 | + link_used_for_test_packets.sut_ingress_port_gnmi_name, |
| 198 | + sut_gnmi_stub.get())); |
| 199 | + |
| 200 | + const int kPacketCount = 100; |
| 201 | + const int kPacketInjectDelayMs = 50; |
| 202 | + for (int i = 0; i < kPacketCount; ++i) { |
| 203 | + LOG(INFO) << "Injecting packet at time: " << absl::Now(); |
| 204 | + ASSERT_OK(pins::InjectEgressPacket( |
| 205 | + /*port=*/kControlSwitchInjectPortP4rtId, |
| 206 | + /*packet=*/raw_packet, |
| 207 | + /*p4info=*/ir_p4info, |
| 208 | + /*p4rt=*/control_p4rt_session.get(), |
| 209 | + /*packet_delay=std::nullopt*/ |
| 210 | + absl::Milliseconds(kPacketInjectDelayMs))); |
| 211 | + } |
| 212 | + // Read packets mirrored back to Control switch. |
| 213 | + std::vector<packetlib::Packet> received_packets; |
| 214 | + EXPECT_OK(control_p4rt_session->HandleNextNStreamMessages( |
| 215 | + [&](const p4::v1::StreamMessageResponse &message) { |
| 216 | + if (!message.has_packet()) |
| 217 | + return false; |
| 218 | + packetlib::Packet received_packet = |
| 219 | + packetlib::ParsePacket(message.packet().payload()); |
| 220 | + received_packets.push_back(received_packet); |
| 221 | + return true; |
| 222 | + }, |
| 223 | + kPacketCount, absl::Minutes(2))); |
| 224 | + // Ensure the correct number of packets was received. |
| 225 | + ASSERT_EQ(received_packets.size(), kPacketCount); |
| 226 | + |
| 227 | + // Validate headers of each received packet. |
| 228 | + int mirrored_packets_received = 0; |
| 229 | + std::optional<uint64_t> prev_obs_time, curr_obs_time; |
| 230 | + std::optional<int> prev_sequence, curr_sequence; |
| 231 | + for (const packetlib::Packet &received_packet : received_packets) { |
| 232 | + // Header count sanity check. |
| 233 | + LOG(INFO) << absl::StrCat("Packet: ", received_packet.DebugString()); |
| 234 | + if (received_packet.headers().size() != 6) { |
| 235 | + continue; |
| 236 | + } |
| 237 | + // Parse Ethernet header. |
| 238 | + ASSERT_EQ(received_packet.headers(0).has_ethernet_header(), true); |
| 239 | + const auto ð_header = received_packet.headers(0).ethernet_header(); |
| 240 | + EXPECT_EQ(eth_header.ethernet_source(), |
| 241 | + mirror_session_params.mirror_encap_src_mac); |
| 242 | + EXPECT_EQ(eth_header.ethernet_destination(), |
| 243 | + mirror_session_params.mirror_encap_dst_mac); |
| 244 | + // Parse VLAN header. |
| 245 | + ASSERT_EQ(received_packet.headers(1).has_vlan_header(), true); |
| 246 | + // Parse IPv6 header. |
| 247 | + ASSERT_EQ(received_packet.headers(2).has_ipv6_header(), true); |
| 248 | + const auto &ip_header = received_packet.headers(2).ipv6_header(); |
| 249 | + EXPECT_EQ(ip_header.ipv6_source(), |
| 250 | + mirror_session_params.mirror_encap_src_ip); |
| 251 | + EXPECT_EQ(ip_header.ipv6_destination(), |
| 252 | + mirror_session_params.mirror_encap_dst_ip); |
| 253 | + // Parse UDP header. |
| 254 | + ASSERT_EQ(received_packet.headers(3).has_udp_header(), true); |
| 255 | + const auto &udp_header = received_packet.headers(3).udp_header(); |
| 256 | + |
| 257 | + EXPECT_EQ(udp_header.source_port(), |
| 258 | + mirror_session_params.mirror_encap_udp_src_port); |
| 259 | + EXPECT_EQ(udp_header.destination_port(), |
| 260 | + mirror_session_params.mirror_encap_udp_dst_port); |
| 261 | + // Parse IPFIX header. |
| 262 | + ASSERT_EQ(received_packet.headers(4).has_ipfix_header(), true); |
| 263 | + // Validate sequence number incrementation. |
| 264 | + |
| 265 | + ASSERT_OK_AND_ASSIGN( |
| 266 | + curr_sequence, |
| 267 | + pdpi::HexStringToInt( |
| 268 | + received_packet.headers(4).ipfix_header().sequence_number())); |
| 269 | + if (prev_sequence.has_value()) { |
| 270 | + EXPECT_EQ(curr_sequence, prev_sequence.value() + 1); |
| 271 | + } |
| 272 | + prev_sequence = curr_sequence; |
| 273 | + // Parse PSAMP header. |
| 274 | + ASSERT_EQ(received_packet.headers(5).has_psamp_header(), true); |
| 275 | + const auto &psamp_header = received_packet.headers(5).psamp_header(); |
| 276 | + // Validate observation times increment within expected range. |
| 277 | + ASSERT_OK_AND_ASSIGN(curr_obs_time, pdpi::HexStringToUint64( |
| 278 | + psamp_header.observation_time())); |
| 279 | + if (prev_obs_time.has_value()) { |
| 280 | + constexpr int kObsTimeGapThresholdNs = 2000000; |
| 281 | + EXPECT_LE( |
| 282 | + (pctutil::ParsePsampHeaderObservationTime(curr_obs_time.value()) - |
| 283 | + pctutil::ParsePsampHeaderObservationTime(prev_obs_time.value())), |
| 284 | + absl::Nanoseconds( |
| 285 | + /*Injection delay*/ (kPacketInjectDelayMs * 1000000) + |
| 286 | + /*2 msecs*/ kObsTimeGapThresholdNs)); |
| 287 | + } |
| 288 | + prev_obs_time = curr_obs_time; |
| 289 | + // Verify ingress port in PSAMP header. |
| 290 | + ASSERT_OK_AND_ASSIGN( |
| 291 | + auto ingress_vendor_port_id, |
| 292 | + pctutil::GetVendorPortId( |
| 293 | + link_used_for_test_packets.sut_ingress_port_gnmi_name, |
| 294 | + sut_gnmi_stub.get())); |
| 295 | + int gnmi_vendor_port_id = -1; |
| 296 | + ASSERT_TRUE(absl::SimpleAtoi(ingress_vendor_port_id, &gnmi_vendor_port_id)); |
| 297 | + ASSERT_OK_AND_ASSIGN(auto psamp_ingress_port_id, |
| 298 | + pdpi::HexStringToInt(psamp_header.ingress_port())); |
| 299 | + EXPECT_EQ(psamp_ingress_port_id, gnmi_vendor_port_id); |
| 300 | + |
| 301 | + LOG(INFO) << absl::StrCat( |
| 302 | + "Ingress port: ", psamp_header.ingress_port(), |
| 303 | + ", Egress port: ", psamp_header.egress_port(), |
| 304 | + ", Observation time: ", psamp_header.observation_time()); |
| 305 | + mirrored_packets_received++; |
| 306 | + } |
| 307 | + |
| 308 | + // Ensure at least 90% of packets was received after being mirrored. |
| 309 | + // Some packets in the 100 packets received could have been spurious. |
| 310 | + ASSERT_GE(mirrored_packets_received, kPacketCount * 0.9); |
| 311 | + // Check ingress and egress port counters increase as expected. That is, |
| 312 | + // within 100-110% of number of packets. |
| 313 | + ASSERT_OK_AND_ASSIGN( |
| 314 | + uint64_t in_packets_post, |
| 315 | + pctutil::GetGnmiStat( |
| 316 | + "in-unicast-pkts", |
| 317 | + link_used_for_test_packets.sut_ingress_port_gnmi_name, |
| 318 | + sut_gnmi_stub.get())); |
| 319 | + // Counter should increment by at least as many packets as were sent. |
| 320 | + EXPECT_GE(in_packets_post, in_packets_pre + kPacketCount); |
| 321 | + // Counter should increment by no more than 110% of packets than were sent. |
| 322 | + // This allows for tolerance of non-test packets such as router solicitation |
| 323 | + // packets and others. |
| 324 | + EXPECT_LE(in_packets_post, in_packets_pre + (kPacketCount * 1.1)); |
| 325 | + LOG(INFO) << absl::StrCat("in_packets_post: ", in_packets_post, |
| 326 | + ", in_packets_pre: ", in_packets_pre); |
| 327 | + |
| 328 | + // SUT egress port gnmi name is hard coded for now, based on the egress port |
| 329 | + // oid that's chosen by the manual component test to set up the mirror |
| 330 | + // session on the SUT. This will be replaced by the egress port that is |
| 331 | + // used when creating that mirror session via P4RT. |
| 332 | + ASSERT_OK_AND_ASSIGN( |
| 333 | + uint64_t out_packets_post, |
| 334 | + pctutil::GetGnmiStat("out-unicast-pkts", |
| 335 | + link_used_for_test_packets.sut_mtp_port_gnmi_name, |
| 336 | + sut_gnmi_stub.get())); |
| 337 | + EXPECT_GE(out_packets_post, out_packets_pre + kPacketCount); |
| 338 | + EXPECT_LE(out_packets_post, out_packets_pre + (kPacketCount * 1.1)); |
| 339 | + LOG(INFO) << absl::StrCat("out_packets_post: ", out_packets_post, |
| 340 | + ", out_packets_pre: ", out_packets_pre); |
| 341 | +} |
| 342 | + |
| 343 | +} // anonymous namespace |
| 344 | +} // namespace pins_test |
0 commit comments