From 688c6295c265e0a0fa0e430cb3614dfa1341e68f Mon Sep 17 00:00:00 2001 From: VSuryaprasad-HCL Date: Thu, 14 Nov 2024 14:42:43 +0530 Subject: [PATCH 1/2] [Dvaas]: Add library that allows DVaaS/Arriba to process user-provided test vectors using a bare-bones, but sane API. --- dvaas/BUILD.bazel | 18 ++++ dvaas/user_provided_packet_test_vector.cc | 113 ++++++++++++++++++++++ dvaas/user_provided_packet_test_vector.h | 59 +++++++++++ 3 files changed, 190 insertions(+) create mode 100644 dvaas/user_provided_packet_test_vector.cc create mode 100644 dvaas/user_provided_packet_test_vector.h diff --git a/dvaas/BUILD.bazel b/dvaas/BUILD.bazel index c38cc3f0..b1541e7e 100644 --- a/dvaas/BUILD.bazel +++ b/dvaas/BUILD.bazel @@ -256,3 +256,21 @@ cc_test( "@com_google_googletest//:gtest_main", ], ) + +cc_library( + name = "user_provided_packet_test_vector", + testonly = True, + srcs = ["user_provided_packet_test_vector.cc"], + hdrs = ["user_provided_packet_test_vector.h"], + deps = [ + ":test_vector", + ":test_vector_cc_proto", + "//gutil:proto", + "//gutil:status", + "//p4_pdpi/packetlib", + "@com_google_absl//absl/status", + "@com_google_absl//absl/status:statusor", + "@com_google_absl//absl/strings", + "@com_google_absl//absl/types:span", + ], +) diff --git a/dvaas/user_provided_packet_test_vector.cc b/dvaas/user_provided_packet_test_vector.cc new file mode 100644 index 00000000..a2f11d7f --- /dev/null +++ b/dvaas/user_provided_packet_test_vector.cc @@ -0,0 +1,113 @@ +#include "dvaas/user_provided_packet_test_vector.h" + +#include +#include + +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "absl/strings/escaping.h" +#include "absl/types/span.h" +#include "dvaas/test_vector.h" +#include "dvaas/test_vector.pb.h" +#include "gutil/proto.h" +#include "gutil/status.h" +#include "p4_pdpi/packetlib/packetlib.h" + +namespace dvaas { + +namespace { + +// Checks that the given `input_packet` is well-formed, returning it with +// omittable fields filled in if that is the case, or an error otherwise. +absl::StatusOr LegitimizePacket(Packet packet) { + RETURN_IF_ERROR( + packetlib::UpdateMissingComputedFields(*packet.mutable_parsed()) + .status()); + RETURN_IF_ERROR(packetlib::ValidatePacket(packet.parsed())); + ASSIGN_OR_RETURN(std::string raw_packet, + packetlib::RawSerializePacket(packet.parsed())); + packet.set_hex(absl::BytesToHexString(raw_packet)); + return packet; +} + +// Checks that the given `vector` is well-formed and if so adds it to +// `legitimized_test_vectors_by_id`, or returns error otherwise. +absl::Status LegitimizeTestVector( + PacketTestVector vector, + PacketTestVectorById& legitimized_test_vectors_by_id) { + if (vector.input().type() != SwitchInput::DATAPLANE) { + return gutil::UnimplementedErrorBuilder() + << "only supported input type is DATAPLANE; found " + << SwitchInput::Type_Name(vector.input().type()); + } + + // Legitimize input packet. + Packet& input_packet = *vector.mutable_input()->mutable_packet(); + ASSIGN_OR_RETURN(int tag, ExtractTestPacketTag(input_packet.parsed()), + _.SetPrepend() << "invalid input packet: "); + ASSIGN_OR_RETURN(input_packet, LegitimizePacket(input_packet), + _.SetPrepend() << "invalid input packet: "); + + // Legitimize acceptable outputs. + if (vector.acceptable_outputs().empty()) { + return gutil::InvalidArgumentErrorBuilder() + << "must specify at least 1 acceptable output, but 0 were found"; + } + for (SwitchOutput& output : *vector.mutable_acceptable_outputs()) { + // Punted output packets are not supported for now. + if (!output.packet_ins().empty()) { + return gutil::UnimplementedErrorBuilder() + << "TODO: support vectors expecting `packet_ins` " + "(punting)"; + } + // Legitimize forwarded output packets. + for (int i = 0; i < output.packets().size(); ++i) { + Packet& output_packet = *output.mutable_packets(i); + ASSIGN_OR_RETURN( + int output_tag, ExtractTestPacketTag(output_packet.parsed()), + _.SetPrepend() << "output packet #" << (i + 1) << " invalid: "); + ASSIGN_OR_RETURN(output_packet, LegitimizePacket(output_packet), + _.SetPrepend() + << "output packet #" << (i + 1) << " invalid: "); + if (output_tag != tag) { + return gutil::InvalidArgumentErrorBuilder() + << "mismatch of input packet tag vs output packet tag for " + "output packet #" + << (i + 1) << ": " << tag << " vs " << output_tag; + } + } + } + + // Add internalized vector to result. + const auto& [it, inserted] = + legitimized_test_vectors_by_id.insert({tag, vector}); + if (!inserted) { + return gutil::InvalidArgumentErrorBuilder() + << "user-provided packet test vectors must be tagged with unique " + "IDs in their payload, but found multiple test vectors with ID " + << tag << ". Dumping offending test vectors:\n<" + << gutil::PrintTextProto(it->second) << ">\n<" + << gutil::PrintTextProto(vector) << ">\n"; + } + return absl::OkStatus(); +} + +} // namespace + +absl::StatusOr LegitimizeUserProvidedTestVectors( + absl::Span user_provided_test_vectors) { + PacketTestVectorById legitimized_test_vectors_by_id; + for (const PacketTestVector& vector : user_provided_test_vectors) { + absl::Status status = + LegitimizeTestVector(vector, legitimized_test_vectors_by_id); + if (!status.ok()) { + return gutil::StatusBuilder(status.code()) + << "problem in user-provided packet test vector: " + << status.message() << "\nDumping offending test vector:\n" + << gutil::PrintTextProto(vector); + } + } + return legitimized_test_vectors_by_id; +} + +} // namespace dvaas diff --git a/dvaas/user_provided_packet_test_vector.h b/dvaas/user_provided_packet_test_vector.h new file mode 100644 index 00000000..c368f0f8 --- /dev/null +++ b/dvaas/user_provided_packet_test_vector.h @@ -0,0 +1,59 @@ +// Empowers users to specify custom packet test vectors that can be validated +// by DVaaS or Arriba. + +// Copyright 2024 Google LLC +// +// 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 +// +// https://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. + +#ifndef PINS_DVAAS_USER_PROVIDED_PACKET_TEST_VECTOR_H_ +#define PINS_DVAAS_USER_PROVIDED_PACKET_TEST_VECTOR_H_ + +#include + +#include "absl/status/statusor.h" +#include "absl/types/span.h" +#include "dvaas/test_vector.h" +#include "dvaas/test_vector.pb.h" + +namespace dvaas { + +// Checks user-provided test vectors for well-formedness and prepares them for +// internal use by DVaaS/Arriba: +// * Fills in "omittable fields", see definition below. +// * Checks that each test vector is "well-formed", see definition below. +// * Returns updated, well-formed test vectors organized by ID, or returns an +// actionable, user-facing error if a test vector is not well-formed. +// +// The following `dvaas::Packet` message fields can be omitted by the user: +// * All `hex` fields. +// * All subfields of `packetlib::Packet` messages that are considered "computed +// fields" by packetlib. This includes checksum and length fields. See the +// packetlib library for the exact definition. +// +// To be "well-formed", a test vector must meet the following requirements: +// * Must specify at least 1 acceptable output. +// * Each input and output packet must: +// * Be valid according to `packetlib::ValidatePacket` after computed fields +// have been filled in. +// * Contain a test packet ID/tag according to `ExtractTestPacketTag`. +// This ID must be: +// * Shared among all packets within the test vector. +// * Unique among all test vectors. +// * The input must be of type `DATAPLANE` (other types may be supported in the +// future). +absl::StatusOr LegitimizeUserProvidedTestVectors( + absl::Span user_provided_test_vectors); + +} // namespace dvaas + +#endif // PINS_DVAAS_USER_PROVIDED_PACKET_TEST_VECTOR_H_ From 3245b29eebc98e2b7447f76985fa7010a8186ce9 Mon Sep 17 00:00:00 2001 From: VSuryaprasad-HCL Date: Tue, 19 Nov 2024 13:56:23 +0530 Subject: [PATCH 2/2] [Dvaas]: Create function to retag packets and update the tag for entire PacketTestVector. --- dvaas/BUILD.bazel | 12 +- dvaas/test_vector.cc | 64 ++++++++++ dvaas/test_vector.h | 11 +- dvaas/test_vector_test.cc | 259 +++++++++++++++++++++++++++++++++++++- 4 files changed, 334 insertions(+), 12 deletions(-) diff --git a/dvaas/BUILD.bazel b/dvaas/BUILD.bazel index b1541e7e..6fa44fd6 100644 --- a/dvaas/BUILD.bazel +++ b/dvaas/BUILD.bazel @@ -143,16 +143,12 @@ cc_library( "//gutil:proto", "//gutil:status", "//p4_pdpi:ir_cc_proto", + "//p4_pdpi/packetlib", "//p4_pdpi/packetlib:packetlib_cc_proto", - "@com_google_absl//absl/container:flat_hash_set", + "@com_google_absl//absl/container:btree", "@com_google_absl//absl/status", "@com_google_absl//absl/status:statusor", "@com_google_absl//absl/strings", - "@com_google_absl//absl/strings:str_format", - "@com_google_absl//absl/types:optional", - "@com_google_absl//absl/types:span", - "@com_google_googletest//:gtest", - "@com_google_protobuf//:protobuf", "@com_googlesource_code_re2//:re2", ], ) @@ -251,8 +247,12 @@ cc_test( srcs = ["test_vector_test.cc"], deps = [ ":test_vector", + ":test_vector_cc_proto", "//gutil:status_matchers", + "//gutil:testing", + "//p4_pdpi/packetlib", "//p4_pdpi/packetlib:packetlib_cc_proto", + "@com_google_absl//absl/status", "@com_google_googletest//:gtest_main", ], ) diff --git a/dvaas/test_vector.cc b/dvaas/test_vector.cc index 3f02e55f..e4a771c1 100644 --- a/dvaas/test_vector.cc +++ b/dvaas/test_vector.cc @@ -17,10 +17,14 @@ #include #include +#include "absl/status/status.h" #include "absl/status/statusor.h" +#include "absl/strings/escaping.h" #include "absl/strings/str_cat.h" +#include "dvaas/test_vector.pb.h" #include "gutil/proto.h" #include "gutil/status.h" +#include "p4_pdpi/packetlib/packetlib.h" #include "p4_pdpi/packetlib/packetlib.pb.h" #include "re2/re2.h" @@ -53,4 +57,64 @@ std::ostream& operator<<(std::ostream& os, const SwitchOutput& output) { return os << output.DebugString(); } +absl::Status UpdateTestTag(packetlib::Packet& packet, int new_tag) { + // Make a new input packet with updated payload. + std::string new_payload = packet.payload(); + if (!RE2::Replace(&new_payload, *kTestPacketIdRegexp, + MakeTestPacketTagFromUniqueId(new_tag))) { + return gutil::InvalidArgumentErrorBuilder() + << "Test packets must contain a tag of the form '" + << kTestPacketIdRegexp->pattern() + << "' in their payload, but the given packet with payload '" + << packet.payload() << "' does not:\n" + << gutil::PrintTextProto(packet); + } + packet.set_payload(new_payload); + bool status; + ASSIGN_OR_RETURN(status, PadPacketToMinimumSize(packet), + _.LogError() << "Failed to pad packet for tag: " << new_tag); + ASSIGN_OR_RETURN(status, UpdateAllComputedFields(packet), + _.LogError() + << "Failed to update payload for tag: " << new_tag); + + return absl::OkStatus(); +} + +// Returns a serialization of the given `packet` as a hexstring. +absl::StatusOr SerializeAsHexString( + const packetlib::Packet& packet) { + ASSIGN_OR_RETURN(std::string serialized_packet, + packetlib::RawSerializePacket(packet), + _ << " where packet = " << packet.DebugString()); + return absl::BytesToHexString(serialized_packet); +} + +absl::Status UpdateTestTag(PacketTestVector& packet_test_vector, int new_tag) { + // Updates the payload of the SwitchInput. + dvaas::Packet& input_packet = + *packet_test_vector.mutable_input()->mutable_packet(); + RETURN_IF_ERROR(UpdateTestTag(*input_packet.mutable_parsed(), new_tag)); + ASSIGN_OR_RETURN(const std::string input_packet_updated_hexstr, + SerializeAsHexString(input_packet.parsed())); + input_packet.set_hex(input_packet_updated_hexstr); + + // Update the payload of the SwitchOutput. + for (SwitchOutput& output_packet : + *packet_test_vector.mutable_acceptable_outputs()) { + for (dvaas::Packet& packet_out : *output_packet.mutable_packets()) { + RETURN_IF_ERROR(UpdateTestTag(*packet_out.mutable_parsed(), new_tag)); + ASSIGN_OR_RETURN(const std::string packet_out_updated_hexstr, + SerializeAsHexString(packet_out.parsed())); + packet_out.set_hex(packet_out_updated_hexstr); + } + for (dvaas::PacketIn& packet_in : *output_packet.mutable_packet_ins()) { + RETURN_IF_ERROR(UpdateTestTag(*packet_in.mutable_parsed(), new_tag)); + ASSIGN_OR_RETURN(const std::string packet_in_updated_hexstr, + SerializeAsHexString(packet_in.parsed())); + packet_in.set_hex(packet_in_updated_hexstr); + } + } + return absl::OkStatus(); +} + } // namespace dvaas diff --git a/dvaas/test_vector.h b/dvaas/test_vector.h index ffbcad09..f0ac3dd3 100644 --- a/dvaas/test_vector.h +++ b/dvaas/test_vector.h @@ -17,16 +17,12 @@ #include #include -#include #include "absl/container/btree_map.h" -#include "absl/container/flat_hash_set.h" +#include "absl/status/status.h" #include "absl/status/statusor.h" -#include "absl/types/optional.h" #include "dvaas/test_vector.pb.h" -#include "google/protobuf/descriptor.h" #include "p4_pdpi/ir.pb.h" -#include "re2/re2.h" namespace dvaas { @@ -47,6 +43,11 @@ absl::StatusOr ExtractTestPacketTag(const packetlib::Packet& packet); // Needed to make gUnit produce human-readable output in open source. std::ostream& operator<<(std::ostream& os, const SwitchOutput& output); +// Updates the test tag (to `new_tag`) and all computed fields of all packets +// (input, acceptable outputs) in the given `packet_test_vectr`. Returns an +// error if the packets are not already tagged. +absl::Status UpdateTestTag(PacketTestVector& packet_test_vector, int new_tag); + using PacketTestVectorById = absl::btree_map; } // namespace dvaas diff --git a/dvaas/test_vector_test.cc b/dvaas/test_vector_test.cc index 28dd28d6..0aa60947 100644 --- a/dvaas/test_vector_test.cc +++ b/dvaas/test_vector_test.cc @@ -1,8 +1,26 @@ +// Copyright 2024 Google LLC +// +// 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 +// +// https://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 "dvaas/test_vector.h" +#include "absl/status/status.h" +#include "dvaas/test_vector.pb.h" #include "gmock/gmock.h" #include "gtest/gtest.h" #include "gutil/status_matchers.h" +#include "gutil/testing.h" +#include "p4_pdpi/packetlib/packetlib.h" #include "p4_pdpi/packetlib/packetlib.pb.h" namespace dvaas { @@ -15,9 +33,248 @@ TEST(MakeTestPacketTag, RoundTripsWithExtractTestPacketTag) { for (int test_packet_id : {0, 1, 2, 42, 1234}) { packetlib::Packet packet; packet.set_payload(MakeTestPacketTagFromUniqueId(test_packet_id)); - EXPECT_THAT(ExtractTestPacketTag(packet), IsOkAndHolds(Eq(test_packet_id))); + ASSERT_THAT(ExtractTestPacketTag(packet), IsOkAndHolds(Eq(test_packet_id))); } } +TEST(UpdateTestPacketTag, YieldsValidPacketTestVectorWithUpdatedTag) { + auto test_vector = gutil::ParseProtoOrDie(R"pb( + input { + type: DATAPLANE + packet { + port: "29" + parsed { + headers { + ethernet_header { + ethernet_destination: "02:1a:0a:d0:62:8b" + ethernet_source: "36:47:08:6f:88:a1" + ethertype: "0x86dd" + } + } + headers { + ipv6_header { + version: "0x6" + dscp: "0x1a" + ecn: "0x0" + flow_label: "0x00000" + payload_length: "0x0025" + next_header: "0x11" + hop_limit: "0x20" + ipv6_source: "2000::" + ipv6_destination: "2800:3f0:c200:800::2000" + } + } + headers { + udp_header { + source_port: "0x0000" + destination_port: "0x03ea" + length: "0x0025" + checksum: "0x3712" + } + } + payload: "test packet #1: Dummy payload" + } + hex: "021a0ad0628b3647086f88a186dd668000000025112020000000000000000000000000000000280003f0c20008000000000000002000000003ea0025371274657374207061636b65742023313a2044756d6d79207061796c6f6164" + } + } + acceptable_outputs { + packets { + port: "12" + parsed { + headers { + ethernet_header { + ethernet_destination: "02:1a:0a:d0:62:8b" + ethernet_source: "36:47:08:6f:88:a1" + ethertype: "0x86dd" + } + } + headers { + ipv6_header { + version: "0x6" + dscp: "0x1a" + ecn: "0x0" + flow_label: "0x00000" + payload_length: "0x0025" + next_header: "0x11" + hop_limit: "0x20" + ipv6_source: "2000::" + ipv6_destination: "2800:3f0:c200:800::2000" + } + } + headers { + udp_header { + source_port: "0x0000" + destination_port: "0x03ea" + length: "0x0025" + checksum: "0x3712" + } + } + payload: "test packet #1: Dummy payload" + } + hex: "021a0ad0628b3647086f88a186dd668000000025112020000000000000000000000000000000280003f0c20008000000000000002000000003ea0025371274657374207061636b65742023313a2044756d6d79207061796c6f6164" + } + packets { + port: "12" + parsed { + headers { + ethernet_header { + ethernet_destination: "02:1a:0a:d0:62:8b" + ethernet_source: "36:47:08:6f:88:a1" + ethertype: "0x86dd" + } + } + headers { + ipv6_header { + version: "0x6" + dscp: "0x1a" + ecn: "0x0" + flow_label: "0x00000" + payload_length: "0x0025" + next_header: "0x11" + hop_limit: "0x20" + ipv6_source: "2000::" + ipv6_destination: "2800:3f0:c200:800::2000" + } + } + headers { + udp_header { + source_port: "0x0000" + destination_port: "0x03ea" + length: "0x0025" + checksum: "0x3712" + } + } + payload: "test packet #1: Dummy payload" + } + hex: "021a0ad0628b3647086f88a186dd668000000025112020000000000000000000000000000000280003f0c20008000000000000002000000003ea0025371274657374207061636b65742023313a2044756d6d79207061796c6f6164" + } + packet_ins { + metadata { + name: "ingress_port" + value { str: "9" } + } + metadata { + name: "target_egress_port" + value { str: "6" } + } + parsed { + headers { + ethernet_header { + ethernet_destination: "02:1a:0a:d0:62:8b" + ethernet_source: "36:47:08:6f:88:a1" + ethertype: "0x86dd" + } + } + headers { + ipv6_header { + version: "0x6" + dscp: "0x1a" + ecn: "0x0" + flow_label: "0x00000" + payload_length: "0x0025" + next_header: "0x11" + hop_limit: "0x20" + ipv6_source: "2000::" + ipv6_destination: "2800:3f0:c200:800::2000" + } + } + headers { + udp_header { + source_port: "0x0000" + destination_port: "0x03ea" + length: "0x0025" + checksum: "0x3712" + } + } + payload: "test packet #1: Dummy payload" + } + hex: "021a0ad0628b3647086f88a186dd668000000025112020000000000000000000000000000000280003f0c20008000000000000002000000003ea0025371274657374207061636b65742023313a2044756d6d79207061796c6f6164" + } + packet_ins { + metadata { + name: "ingress_port" + value { str: "9" } + } + metadata { + name: "target_egress_port" + value { str: "6" } + } + parsed { + headers { + ethernet_header { + ethernet_destination: "02:1a:0a:d0:62:8b" + ethernet_source: "36:47:08:6f:88:a1" + ethertype: "0x86dd" + } + } + headers { + ipv6_header { + version: "0x6" + dscp: "0x1a" + ecn: "0x0" + flow_label: "0x00000" + payload_length: "0x0025" + next_header: "0x11" + hop_limit: "0x20" + ipv6_source: "2000::" + ipv6_destination: "2800:3f0:c200:800::2000" + } + } + headers { + udp_header { + source_port: "0x0000" + destination_port: "0x03ea" + length: "0x0025" + checksum: "0x3712" + } + } + payload: "test packet #1: Dummy payload" + } + hex: "021a0ad0628b3647086f88a186dd668000000025112020000000000000000000000000000000280003f0c20008000000000000002000000003ea0025371274657374207061636b65742023313a2044756d6d79207061796c6f6164" + } + } + )pb"); + PacketTestVector updated_test_vector = test_vector; + int kNewTag = 2000000; + ASSERT_OK(UpdateTestTag(test_vector, kNewTag)); + + // Check if all the tags were updated, including the hex and payload. + ASSERT_OK(packetlib::ValidatePacket(test_vector.input().packet().parsed())); + ASSERT_THAT(ExtractTestPacketTag(test_vector.input().packet().parsed()), + IsOkAndHolds(Eq(kNewTag))); + ASSERT_NE(test_vector.input().packet().hex(), + updated_test_vector.input().packet().hex()); + for (int i = 0; i < test_vector.acceptable_outputs().size(); ++i) { + const SwitchOutput& acceptable_outputs = test_vector.acceptable_outputs(i); + for (int j = 0; j < acceptable_outputs.packets().size(); ++j) { + const Packet& packet = acceptable_outputs.packets(j); + ASSERT_OK(packetlib::ValidatePacket(packet.parsed())); + ASSERT_THAT(ExtractTestPacketTag(packet.parsed()), + IsOkAndHolds(Eq(kNewTag))); + ASSERT_NE(packet.hex(), + updated_test_vector.acceptable_outputs(i).packets(j).hex()); + } + for (int j = 0; j < acceptable_outputs.packet_ins().size(); ++j) { + const PacketIn& packet_in = acceptable_outputs.packet_ins(j); + ASSERT_OK(packetlib::ValidatePacket(packet_in.parsed())); + ASSERT_THAT(ExtractTestPacketTag(packet_in.parsed()), + IsOkAndHolds(Eq(kNewTag))); + ASSERT_NE(packet_in.hex(), + updated_test_vector.acceptable_outputs(i).packet_ins(j).hex()); + } + } +} + +TEST(UpdateTestPacketTag, FailsForPacketWithNoTag) { + auto test_vector = gutil::ParseProtoOrDie(R"pb( + input { + type: DATAPLANE + packet { parsed { payload: "test packet" } } + } + )pb"); + ASSERT_THAT(UpdateTestTag(test_vector, /*new_tag=*/0), + gutil::StatusIs(absl::StatusCode::kInvalidArgument)); +} + } // namespace } // namespace dvaas