From 06a93b090a63609fd99868029a3b1af09e137149 Mon Sep 17 00:00:00 2001 From: VSuryaprasad-hcl <159443973+VSuryaprasad-HCL@users.noreply.github.com> Date: Fri, 13 Dec 2024 02:41:10 +0000 Subject: [PATCH 1/9] [Dvaas] Make DVaaS users of PacketSynthesizer pass time limit base on testing needs (#811) Co-authored-by: kishanps --- dvaas/BUILD.bazel | 82 ++++++++++++++++++----------------- dvaas/dataplane_validation.cc | 48 ++++++++++++-------- dvaas/dataplane_validation.h | 35 ++++++++++----- dvaas/traffic_generator.cc | 14 +++--- dvaas/traffic_generator.h | 10 ++--- dvaas/validation_result.cc | 6 ++- dvaas/validation_result.h | 15 ++++++- 7 files changed, 127 insertions(+), 83 deletions(-) diff --git a/dvaas/BUILD.bazel b/dvaas/BUILD.bazel index 8159b06c2..0da363694 100644 --- a/dvaas/BUILD.bazel +++ b/dvaas/BUILD.bazel @@ -20,6 +20,47 @@ package( licenses = ["notice"], ) +cc_library( + name = "dataplane_validation", + srcs = ["dataplane_validation.cc"], + hdrs = ["dataplane_validation.h"], + deps = [ + ":output_writer", + ":packet_injection", + ":port_id_map", + ":switch_api", + ":test_run_validation", + ":test_vector", + ":test_vector_cc_proto", + ":user_provided_packet_test_vector", + ":validation_result", + "//gutil:status", + "//gutil:test_artifact_writer", + "//gutil:version", + "//lib/gnmi:gnmi_helper", + "//lib/gnmi:openconfig_cc_proto", + "//lib/p4rt:p4rt_port", + "//p4_pdpi:ir_cc_proto", + "//p4_pdpi:p4_runtime_session", + "//p4_pdpi:p4_runtime_session_extras", + "//p4_pdpi/packetlib:packetlib_cc_proto", + "//p4_symbolic/packet_synthesizer:packet_synthesizer_cc_proto", + "//tests/lib:switch_test_setup_helpers", + "//thinkit:mirror_testbed", + "@com_github_gnmi//proto/gnmi:gnmi_cc_proto", + "@com_github_google_glog//:glog", + "@com_github_p4lang_p4runtime//:p4runtime_cc_proto", + "@com_google_absl//absl/container:flat_hash_map", + "@com_google_absl//absl/container:flat_hash_set", + "@com_google_absl//absl/status", + "@com_google_absl//absl/status:statusor", + "@com_google_absl//absl/strings", + "@com_google_absl//absl/time", + "@com_google_absl//absl/types:span", + "@com_google_protobuf//:protobuf", + ], +) + cc_library( name = "validation_result", srcs = ["validation_result.cc"], @@ -29,6 +70,7 @@ cc_library( ":test_vector_cc_proto", ":test_vector_stats", "//gutil:status", + "//p4_symbolic/packet_synthesizer:packet_synthesizer_cc_proto", "@com_github_google_glog//:glog", "@com_google_absl//absl/algorithm:container", "@com_google_absl//absl/container:flat_hash_set", @@ -358,43 +400,3 @@ cmd_diff_test( expected = ":test_run_validation_test.expected.output", tools = [":test_run_validation_test_runner"], ) - -cc_library( - name = "dataplane_validation", - srcs = ["dataplane_validation.cc"], - hdrs = ["dataplane_validation.h"], - deps = [ - ":output_writer", - ":packet_injection", - ":port_id_map", - ":switch_api", - ":test_run_validation", - ":test_vector", - ":test_vector_cc_proto", - ":user_provided_packet_test_vector", - ":validation_result", - "//gutil:status", - "//gutil:test_artifact_writer", - "//gutil:version", - "//lib/gnmi:gnmi_helper", - "//lib/gnmi:openconfig_cc_proto", - "//lib/p4rt:p4rt_port", - "//p4_pdpi:ir_cc_proto", - "//p4_pdpi:p4_runtime_session", - "//p4_pdpi:p4_runtime_session_extras", - "//p4_pdpi/packetlib:packetlib_cc_proto", - "//p4_symbolic/packet_synthesizer:packet_synthesizer_cc_proto", - "//tests/lib:switch_test_setup_helpers", - "//thinkit:mirror_testbed", - "@com_github_gnmi//proto/gnmi:gnmi_cc_proto", - "@com_github_google_glog//:glog", - "@com_github_p4lang_p4runtime//:p4runtime_cc_proto", - "@com_google_absl//absl/container:flat_hash_map", - "@com_google_absl//absl/container:flat_hash_set", - "@com_google_absl//absl/status", - "@com_google_absl//absl/status:statusor", - "@com_google_absl//absl/strings", - "@com_google_absl//absl/types:span", - "@com_google_protobuf//:protobuf", - ], -) diff --git a/dvaas/dataplane_validation.cc b/dvaas/dataplane_validation.cc index a8a6b860a..032e569c9 100644 --- a/dvaas/dataplane_validation.cc +++ b/dvaas/dataplane_validation.cc @@ -165,7 +165,7 @@ absl::StatusOr InferP4Specification( // Generates and returns test vectors using the backend functions // `SynthesizePackets` and `GeneratePacketTestVectors`. Reads the table entries, // P4Info, and relevant P4RT port IDs from the switch. -absl::StatusOr GenerateTestVectors( +absl::StatusOr GenerateTestVectors( const DataplaneValidationParams& params, SwitchApi& sut, DataplaneValidationBackend& backend, gutil::TestArtifactWriter& writer) { // Determine the P4 specification to test against. @@ -190,24 +190,35 @@ absl::StatusOr GenerateTestVectors( } const P4rtPortId& default_ingress_port = ports[0]; + GenerateTestVectorsResult generate_test_vectors_result; + // Synthesize test packets. LOG(INFO) << "Synthesizing test packets"; - ASSIGN_OR_RETURN( - std::vector synthesized_packets, - backend.SynthesizePackets(ir_p4info, entries, p4_spec.p4_symbolic_config, - ports, [&](absl::string_view stats) { - return writer.AppendToTestArtifact( - "test_packet_stats.txt", stats); - })); - RETURN_IF_ERROR(writer.AppendToTestArtifact("synthesized_packets.txt", - ToString(synthesized_packets))); + ASSIGN_OR_RETURN(generate_test_vectors_result.packet_synthesis_result, + backend.SynthesizePackets( + ir_p4info, entries, p4_spec.p4_symbolic_config, ports, + [&](absl::string_view stats) { + return writer.AppendToTestArtifact( + "test_packet_stats.txt", stats); + }, + params.time_limit)); + + RETURN_IF_ERROR(writer.AppendToTestArtifact( + "synthesized_packets.txt", + ToString(generate_test_vectors_result.packet_synthesis_result + .synthesized_packets))); // Generate test vectors with output prediction from the synthesized // packets. LOG(INFO) << "Generating test vectors with output prediction"; - return backend.GeneratePacketTestVectors( - ir_p4info, entries, p4_spec.bmv2_config, ports, synthesized_packets, - default_ingress_port); + ASSIGN_OR_RETURN(generate_test_vectors_result.packet_test_vector_by_id, + backend.GeneratePacketTestVectors( + ir_p4info, entries, p4_spec.bmv2_config, ports, + generate_test_vectors_result.packet_synthesis_result + .synthesized_packets, + default_ingress_port)); + + return generate_test_vectors_result; } absl::StatusOr DataplaneValidator::ValidateDataplane( @@ -246,10 +257,12 @@ absl::StatusOr DataplaneValidator::ValidateDataplane( mirror_testbed_port_map, *sut.gnmi, *control_switch.gnmi, *writer)); // Generate test vectors. - PacketTestVectorById test_vectors; + GenerateTestVectorsResult generate_test_vectors_result; + PacketTestVectorById& test_vectors = + generate_test_vectors_result.packet_test_vector_by_id; if (params.packet_test_vector_override.empty()) { LOG(INFO) << "Auto-generating test vectors"; - ASSIGN_OR_RETURN(test_vectors, + ASSIGN_OR_RETURN(generate_test_vectors_result, GenerateTestVectors(params, sut, *backend_, *writer)); } else { LOG(INFO) << "Checking user-provided test vectors for well-formedness"; @@ -279,8 +292,9 @@ absl::StatusOr DataplaneValidator::ValidateDataplane( RETURN_IF_ERROR(writer->AppendToTestArtifact("test_runs.textproto", test_runs.DebugString())); - ValidationResult validation_result(std::move(test_runs), - params.switch_output_diff_params); + ValidationResult validation_result( + std::move(test_runs), params.switch_output_diff_params, + generate_test_vectors_result.packet_synthesis_result); RETURN_IF_ERROR(writer->AppendToTestArtifact( "test_vector_failures.txt", absl::StrJoin(validation_result.GetAllFailures(), "\n\n"))); diff --git a/dvaas/dataplane_validation.h b/dvaas/dataplane_validation.h index c78807cf1..8dd675118 100644 --- a/dvaas/dataplane_validation.h +++ b/dvaas/dataplane_validation.h @@ -29,6 +29,7 @@ limitations under the License. #include "absl/container/flat_hash_map.h" #include "absl/container/flat_hash_set.h" #include "absl/status/statusor.h" +#include "absl/time/time.h" #include "absl/types/span.h" #include "dvaas/output_writer.h" #include "dvaas/packet_injection.h" @@ -102,6 +103,12 @@ struct DataplaneValidationParams { // NOTE: Not required for valid mirror testbeds. This is a workaround for // non-standard testbeds only. std::optional mirror_testbed_port_map_override; + + // The 'time_limit' sets the maximum allowed time for dataplane validation to + // synthesize test packets. If nullopt, packet synthesizer runs to completion + // for its coverage goals. Otherwise, if packet synthesis timed out, the + // synthesis results cover the coverage goals only partially. + std::optional time_limit = std::nullopt; }; // Forward declaration. See below for description. @@ -213,13 +220,12 @@ class DataplaneValidationBackend { // specific packet synthesis implementation (our current implementation is not // even open-source yet), so DVaaS takes the synthesis function as an input // parameter. - virtual absl::StatusOr< - std::vector> - SynthesizePackets(const pdpi::IrP4Info& ir_p4info, - const pdpi::IrTableEntries& ir_entries, - const p4::v1::ForwardingPipelineConfig& p4_symbolic_config, - absl::Span ports, - const OutputWriterFunctionType& write_stats) const = 0; + virtual absl::StatusOr SynthesizePackets( + const pdpi::IrP4Info& ir_p4info, const pdpi::IrTableEntries& ir_entries, + const p4::v1::ForwardingPipelineConfig& p4_symbolic_config, + absl::Span ports, + const OutputWriterFunctionType& write_stats, + std::optional time_limit = std::nullopt) const = 0; // Generates a map of test ID to PacketTestVector with output prediction // given a list of `synthesized_packets` for the given input (program, @@ -267,10 +273,17 @@ class DataplaneValidationBackend { virtual ~DataplaneValidationBackend() = default; }; -// Generates and returns test vectors using the backend functions -// `SynthesizePackets` and `GeneratePacketTestVectors`. Reads the table entries, -// P4Info, and relevant P4RT port IDs from the switch. -absl::StatusOr GenerateTestVectors( +// Stores test vectors as well as the result of automated test packet synthesis +// (if any). +struct GenerateTestVectorsResult { + PacketTestVectorById packet_test_vector_by_id; + PacketSynthesisResult packet_synthesis_result; +}; + +// Generates and returns test vectors as well as packet synthesis result using +// the backend functions `SynthesizePackets` and `GeneratePacketTestVectors`. +// Reads the table entries, P4Info, and relevant P4RT port IDs from the switch. +absl::StatusOr GenerateTestVectors( const DataplaneValidationParams& params, SwitchApi& sut, DataplaneValidationBackend& backend, gutil::TestArtifactWriter& writer); diff --git a/dvaas/traffic_generator.cc b/dvaas/traffic_generator.cc index 229427ab4..ccb3118c0 100644 --- a/dvaas/traffic_generator.cc +++ b/dvaas/traffic_generator.cc @@ -91,7 +91,7 @@ absl::StatusOr SimpleTrafficGenerator::Init( // Generate test vectors. gutil::BazelTestArtifactWriter writer; ASSIGN_OR_RETURN( - test_vector_by_id_, + generate_test_vectors_result_, GenerateTestVectors(params.validation_params, testbed_configurator_->SutApi(), *backend_, writer)); @@ -152,7 +152,8 @@ void SimpleTrafficGenerator::InjectTraffic() { PacketStatistics statistics; absl::StatusOr test_runs = SendTestPacketsAndCollectOutputs( *testbed_configurator_->SutApi().p4rt, - *testbed_configurator_->ControlSwitchApi().p4rt, test_vector_by_id_, + *testbed_configurator_->ControlSwitchApi().p4rt, + generate_test_vectors_result_.packet_test_vector_by_id, { .max_packets_to_send_per_second = params_.validation_params.max_packets_to_send_per_second, @@ -176,10 +177,9 @@ absl::StatusOr SimpleTrafficGenerator::GetValidationResult() { test_runs_mutex_.Lock(); PacketTestRuns test_runs = test_runs_; test_runs_mutex_.Unlock(); - return ValidationResult( - test_runs, params_.validation_params.ignored_fields_for_validation, - params_.validation_params.ignored_metadata_for_validation); + test_runs, params_.validation_params.switch_output_diff_params, + generate_test_vectors_result_.packet_synthesis_result); } absl::StatusOr @@ -190,8 +190,8 @@ SimpleTrafficGenerator::GetAndClearValidationResult() { test_runs_mutex_.Unlock(); return ValidationResult( - test_runs, params_.validation_params.ignored_fields_for_validation, - params_.validation_params.ignored_metadata_for_validation); + test_runs, params_.validation_params.switch_output_diff_params, + generate_test_vectors_result_.packet_synthesis_result); } } // namespace dvaas diff --git a/dvaas/traffic_generator.h b/dvaas/traffic_generator.h index c0fe8355d..f50da5be3 100644 --- a/dvaas/traffic_generator.h +++ b/dvaas/traffic_generator.h @@ -157,7 +157,7 @@ class SimpleTrafficGenerator : public TrafficGenerator { std::unique_ptr testbed_configurator_; // Test vectors created as a result of (latest) call to `Init`. Calls to // `StartTraffic` use these test vectors. - PacketTestVectorById test_vector_by_id_; + GenerateTestVectorsResult generate_test_vectors_result_; enum State { // The object has been created but `Init` has not been called. @@ -185,10 +185,10 @@ class SimpleTrafficGenerator : public TrafficGenerator { std::thread traffic_injection_thread_; // Runs in a separate thread, as a loop that injects and collects packets // until traffic is stopped. - // In each iteration of the loop, injects packets in `test_vector_by_id_` at - // the rate specified by `params_`. At the end of each iteration, WAITS UP TO - // 3 SECONDS to collect any in-flight packets, before moving on to next - // iteration. + // In each iteration of the loop, injects packets in + // `generate_test_vectors_result_.packet_test_vector_by_id` at the rate + // specified by `params_`. At the end of each iteration, WAITS UP TO 3 SECONDS + // to collect any in-flight packets, before moving on to next iteration. void InjectTraffic() ABSL_LOCKS_EXCLUDED(test_runs_mutex_); // Result of packet injection and collection (i.e. test vector + switch diff --git a/dvaas/validation_result.cc b/dvaas/validation_result.cc index 45e24c100..eceb86fe9 100644 --- a/dvaas/validation_result.cc +++ b/dvaas/validation_result.cc @@ -29,8 +29,9 @@ namespace dvaas { -ValidationResult::ValidationResult(const PacketTestRuns& test_runs, - const SwitchOutputDiffParams& diff_params) { +ValidationResult::ValidationResult( + const PacketTestRuns& test_runs, const SwitchOutputDiffParams& diff_params, + const PacketSynthesisResult& packet_synthesis_result) { test_outcomes_.mutable_outcomes()->Reserve(test_runs.test_runs_size()); for (const auto& test_run : test_runs.test_runs()) { PacketTestOutcome& outcome = *test_outcomes_.add_outcomes(); @@ -39,6 +40,7 @@ ValidationResult::ValidationResult(const PacketTestRuns& test_runs, } test_vector_stats_ = ComputeTestVectorStats(test_outcomes_); + packet_synthesis_result_ = packet_synthesis_result; } absl::Status ValidationResult::HasSuccessRateOfAtLeast( diff --git a/dvaas/validation_result.h b/dvaas/validation_result.h index c60f03617..7aff28543 100644 --- a/dvaas/validation_result.h +++ b/dvaas/validation_result.h @@ -28,9 +28,20 @@ #include "dvaas/test_vector.pb.h" #include "dvaas/test_vector_stats.h" #include "google/protobuf/descriptor.h" +#include "p4_symbolic/packet_synthesizer/packet_synthesizer.pb.h" namespace dvaas { +// Result of automated test packet synthesis (using P4-Symbolic) +struct PacketSynthesisResult { + std::vector + synthesized_packets; + // True if and only if packet synthesis runs with a time limit and does not + // finish within that time limit. If true, `synthesized_packets` may not + // fully cover the target coverage goals. + bool packet_synthesis_timed_out; +}; + // The result of dataplane validation, as returned to DVaaS users. class [[nodiscard]] ValidationResult { public: @@ -62,11 +73,13 @@ class [[nodiscard]] ValidationResult { // `ignored_fields` and `ignored_metadata` during validation, see // `test_run_validation.h` for details. ValidationResult(const PacketTestRuns& test_runs, - const SwitchOutputDiffParams& diff_params); + const SwitchOutputDiffParams& diff_params, + const PacketSynthesisResult& packet_synthesis_result); private: PacketTestOutcomes test_outcomes_; TestVectorStats test_vector_stats_; + PacketSynthesisResult packet_synthesis_result_; }; } // namespace dvaas From b2f63fcbbf410abcb53cff137284d6daa0c87a65 Mon Sep 17 00:00:00 2001 From: VSuryaprasad-hcl <159443973+VSuryaprasad-HCL@users.noreply.github.com> Date: Fri, 13 Dec 2024 02:42:59 +0000 Subject: [PATCH 2/9] [Dvaas] Rename time_limit to packet_synthesis_time_limit. Create function to access packet_synthesis_timed_out in ValidationResult. Add more comments to PacketSynthesizerTimedOut.Add support for non-entry entities. (#813) Co-authored-by: kishanps --- dvaas/dataplane_validation.cc | 25 +++++++++++++------------ dvaas/dataplane_validation.h | 14 +++++++------- dvaas/validation_result.cc | 4 ++++ dvaas/validation_result.h | 10 +++++++++- 4 files changed, 33 insertions(+), 20 deletions(-) diff --git a/dvaas/dataplane_validation.cc b/dvaas/dataplane_validation.cc index 032e569c9..6592abdad 100644 --- a/dvaas/dataplane_validation.cc +++ b/dvaas/dataplane_validation.cc @@ -34,6 +34,7 @@ #include "dvaas/user_provided_packet_test_vector.h" #include "dvaas/validation_result.h" #include "glog/logging.h" +#include "gutil/proto.h" #include "gutil/status.h" #include "gutil/test_artifact_writer.h" #include "gutil/version.h" @@ -176,10 +177,10 @@ absl::StatusOr GenerateTestVectors( RETURN_IF_ERROR(writer.AppendToTestArtifact( "sut_bmv2_config.txt", p4_spec.bmv2_config.DebugString())); - // Read P4Info and entries from SUT, sorted for determinism. + // Read P4Info and control plane entities from SUT, sorted for determinism. ASSIGN_OR_RETURN(pdpi::IrP4Info ir_p4info, pdpi::GetIrP4Info(*sut.p4rt)); - ASSIGN_OR_RETURN(pdpi::IrTableEntries entries, - pdpi::ReadIrTableEntriesSorted(*sut.p4rt)); + ASSIGN_OR_RETURN(pdpi::IrEntities entities, + pdpi::ReadIrEntitiesSorted(*sut.p4rt)); // Get enabled Ethernet ports from SUT's GNMI config. ASSIGN_OR_RETURN(std::vector ports, @@ -196,12 +197,12 @@ absl::StatusOr GenerateTestVectors( LOG(INFO) << "Synthesizing test packets"; ASSIGN_OR_RETURN(generate_test_vectors_result.packet_synthesis_result, backend.SynthesizePackets( - ir_p4info, entries, p4_spec.p4_symbolic_config, ports, + ir_p4info, entities, p4_spec.p4_symbolic_config, ports, [&](absl::string_view stats) { return writer.AppendToTestArtifact( "test_packet_stats.txt", stats); }, - params.time_limit)); + params.packet_synthesis_time_limit)); RETURN_IF_ERROR(writer.AppendToTestArtifact( "synthesized_packets.txt", @@ -213,7 +214,7 @@ absl::StatusOr GenerateTestVectors( LOG(INFO) << "Generating test vectors with output prediction"; ASSIGN_OR_RETURN(generate_test_vectors_result.packet_test_vector_by_id, backend.GeneratePacketTestVectors( - ir_p4info, entries, p4_spec.bmv2_config, ports, + ir_p4info, entities, p4_spec.bmv2_config, ports, generate_test_vectors_result.packet_synthesis_result .synthesized_packets, default_ingress_port)); @@ -234,8 +235,8 @@ absl::StatusOr DataplaneValidator::ValidateDataplane( ASSIGN_OR_RETURN(pdpi::IrP4Info ir_info, pdpi::GetIrP4Info(*control_switch.p4rt)); - // Clear control switch table entries and install punt entries instead. - RETURN_IF_ERROR(pdpi::ClearTableEntries(control_switch.p4rt.get())); + // Clear control switch entities and install punt entries instead. + RETURN_IF_ERROR(pdpi::ClearEntities(*control_switch.p4rt)); ASSIGN_OR_RETURN(pdpi::IrEntities punt_entries, backend_->GetEntitiesToPuntAllPackets(ir_info)); RETURN_IF_ERROR( @@ -243,10 +244,10 @@ absl::StatusOr DataplaneValidator::ValidateDataplane( } // Read and store table entries on SUT as an artifact. - ASSIGN_OR_RETURN(pdpi::IrTableEntries entries, - pdpi::ReadIrTableEntriesSorted(*sut.p4rt)); - RETURN_IF_ERROR(writer->AppendToTestArtifact("sut_ir_table_entries.txt", - entries.DebugString())); + ASSIGN_OR_RETURN(pdpi::IrEntities entities, + pdpi::ReadIrEntitiesSorted(*sut.p4rt)); + RETURN_IF_ERROR(writer->AppendToTestArtifact( + "sut_ir_entities.txtpb", gutil::PrintTextProto(entities))); // Store port mapping as an artifact (identity if not given a value). MirrorTestbedP4rtPortIdMap mirror_testbed_port_map = diff --git a/dvaas/dataplane_validation.h b/dvaas/dataplane_validation.h index 8dd675118..1936e30ce 100644 --- a/dvaas/dataplane_validation.h +++ b/dvaas/dataplane_validation.h @@ -104,11 +104,11 @@ struct DataplaneValidationParams { // non-standard testbeds only. std::optional mirror_testbed_port_map_override; - // The 'time_limit' sets the maximum allowed time for dataplane validation to - // synthesize test packets. If nullopt, packet synthesizer runs to completion - // for its coverage goals. Otherwise, if packet synthesis timed out, the - // synthesis results cover the coverage goals only partially. - std::optional time_limit = std::nullopt; + // Maximum allowed time for dataplane validation to synthesize test packets. + // If nullopt, packet synthesizer runs to completion for its coverage goals. + // Otherwise, if packet synthesis timed out, the synthesis results cover the + // coverage goals only partially. + std::optional packet_synthesis_time_limit = std::nullopt; }; // Forward declaration. See below for description. @@ -221,7 +221,7 @@ class DataplaneValidationBackend { // even open-source yet), so DVaaS takes the synthesis function as an input // parameter. virtual absl::StatusOr SynthesizePackets( - const pdpi::IrP4Info& ir_p4info, const pdpi::IrTableEntries& ir_entries, + const pdpi::IrP4Info& ir_p4info, const pdpi::IrEntities& ir_entities, const p4::v1::ForwardingPipelineConfig& p4_symbolic_config, absl::Span ports, const OutputWriterFunctionType& write_stats, @@ -243,7 +243,7 @@ class DataplaneValidationBackend { // 3. The packet will be padded to minimum size and the computed fields // recomputed. virtual absl::StatusOr GeneratePacketTestVectors( - const pdpi::IrP4Info& ir_p4info, const pdpi::IrTableEntries& ir_entries, + const pdpi::IrP4Info& ir_p4info, const pdpi::IrEntities& ir_entities, const p4::v1::ForwardingPipelineConfig& bmv2_config, absl::Span ports, std::vector& diff --git a/dvaas/validation_result.cc b/dvaas/validation_result.cc index eceb86fe9..63805b5e1 100644 --- a/dvaas/validation_result.cc +++ b/dvaas/validation_result.cc @@ -87,4 +87,8 @@ std::vector ValidationResult::GetAllFailures() const { return failures; } +bool ValidationResult::PacketSynthesizerTimedOut() const { + return packet_synthesis_result_.packet_synthesis_timed_out; +} + } // namespace dvaas diff --git a/dvaas/validation_result.h b/dvaas/validation_result.h index 7aff28543..a1efef14f 100644 --- a/dvaas/validation_result.h +++ b/dvaas/validation_result.h @@ -39,7 +39,7 @@ struct PacketSynthesisResult { // True if and only if packet synthesis runs with a time limit and does not // finish within that time limit. If true, `synthesized_packets` may not // fully cover the target coverage goals. - bool packet_synthesis_timed_out; + bool packet_synthesis_timed_out = false; }; // The result of dataplane validation, as returned to DVaaS users. @@ -76,6 +76,14 @@ class [[nodiscard]] ValidationResult { const SwitchOutputDiffParams& diff_params, const PacketSynthesisResult& packet_synthesis_result); + // Returns true if and only if packet synthesis runs with a time limit and + // does not finish within that time limit. + // NOTE: If true, dataplane validation did not fully cover the target coverage + // goals (in the worst case, it may have not tested dataplane at all). This + // should be taken into account when interpreting the results of other + // functions like `HasSuccessRateOfAtLeast` and `GetAllFailures`. + bool PacketSynthesizerTimedOut() const; + private: PacketTestOutcomes test_outcomes_; TestVectorStats test_vector_stats_; From 3a90d31299c10752302341a8c8dccc2b589f5c96 Mon Sep 17 00:00:00 2001 From: VSuryaprasad-hcl <159443973+VSuryaprasad-HCL@users.noreply.github.com> Date: Fri, 13 Dec 2024 02:43:40 +0000 Subject: [PATCH 3/9] [Dvaas] Add parameter to ignore SUT packet-ins to be able to tolerate SUT P4RT disconnections. (#815) Co-authored-by: kishanps --- dvaas/packet_injection.cc | 63 ++++++++++++++++++++------------------ dvaas/packet_injection.h | 6 ++++ dvaas/traffic_generator.cc | 3 ++ 3 files changed, 42 insertions(+), 30 deletions(-) diff --git a/dvaas/packet_injection.cc b/dvaas/packet_injection.cc index 57a78bc2f..4e82aaf25 100644 --- a/dvaas/packet_injection.cc +++ b/dvaas/packet_injection.cc @@ -136,11 +136,6 @@ absl::StatusOr SendTestPacketsAndCollectOutputs( << packet_test_vector_by_id.size(); statistics.total_packets_injected += packet_test_vector_by_id.size(); - // Get IrP4Infos. - ASSIGN_OR_RETURN(const pdpi::IrP4Info sut_ir_p4info, GetIrP4Info(sut)); - ASSIGN_OR_RETURN(const pdpi::IrP4Info control_ir_p4info, - GetIrP4Info(control_switch)); - // Compute per packet injection delay. std::optional injection_delay; if (parameters.max_packets_to_send_per_second.has_value()) { @@ -149,6 +144,8 @@ absl::StatusOr SendTestPacketsAndCollectOutputs( } // Send packets. + ASSIGN_OR_RETURN(const pdpi::IrP4Info control_ir_p4info, + GetIrP4Info(control_switch)); for (const auto& [test_id, packet_test_vector] : packet_test_vector_by_id) { if (packet_test_vector.input().type() == SwitchInput::DATAPLANE) { const Packet& packet = packet_test_vector.input().packet(); @@ -174,7 +171,7 @@ absl::StatusOr SendTestPacketsAndCollectOutputs( } LOG(INFO) << "Finished injecting test packets"; - // Check the output of the switches. + // Check the output of the control switch. const absl::Duration kCollectionDuration = absl::Seconds(3); absl::StatusOr> control_packet_ins = CollectStreamMessageResponsesAndReturnTaggedPacketIns( @@ -186,15 +183,6 @@ absl::StatusOr SendTestPacketsAndCollectOutputs( << " forwarded packets (from control switch)"; statistics.total_packets_forwarded += control_packet_ins->size(); - absl::StatusOr> sut_packet_ins = - CollectStreamMessageResponsesAndReturnTaggedPacketIns( - sut, kCollectionDuration, parameters.is_expected_unsolicited_packet); - RETURN_IF_ERROR(sut_packet_ins.status()) - << "while collecting the output of SUT"; - LOG(INFO) << "Collected " << sut_packet_ins->size() - << " punted packets (from SUT)"; - statistics.total_packets_punted += sut_packet_ins->size(); - absl::btree_map switch_output_by_id; // Processing the output of the control switch. for (const TaggedPacketIn& packet_in : *control_packet_ins) { @@ -217,21 +205,36 @@ absl::StatusOr SendTestPacketsAndCollectOutputs( *forwarded_output.mutable_port() = sut_egress_port.GetP4rtEncoding(); } - // Processing the output of SUT. - for (const TaggedPacketIn& packet_in : *sut_packet_ins) { - // Add to (punted) switch output for ID. - PacketIn& punted_output = - *switch_output_by_id[packet_in.tag].add_packet_ins(); - - // Set hex and parsed packet. - punted_output.set_hex( - absl::BytesToHexString(packet_in.packet_in.payload())); - *punted_output.mutable_parsed() = packet_in.parsed_inner_packet; - - // Set metadata. - ASSIGN_OR_RETURN(pdpi::IrPacketIn ir_packet_in, - pdpi::PiPacketInToIr(sut_ir_p4info, packet_in.packet_in)); - *punted_output.mutable_metadata() = ir_packet_in.metadata(); + if (parameters.enable_sut_packet_in_collection) { + // Check the output of SUT. + absl::StatusOr> sut_packet_ins = + CollectStreamMessageResponsesAndReturnTaggedPacketIns( + sut, kCollectionDuration, + parameters.is_expected_unsolicited_packet); + RETURN_IF_ERROR(sut_packet_ins.status()) + << "while collecting the output of SUT"; + LOG(INFO) << "Collected " << sut_packet_ins->size() + << " punted packets (from SUT)"; + statistics.total_packets_punted += sut_packet_ins->size(); + + // Processing the output of SUT. + ASSIGN_OR_RETURN(const pdpi::IrP4Info sut_ir_p4info, GetIrP4Info(sut)); + for (const TaggedPacketIn& packet_in : *sut_packet_ins) { + // Add to (punted) switch output for ID. + PacketIn& punted_output = + *switch_output_by_id[packet_in.tag].add_packet_ins(); + + // Set hex and parsed packet. + punted_output.set_hex( + absl::BytesToHexString(packet_in.packet_in.payload())); + *punted_output.mutable_parsed() = packet_in.parsed_inner_packet; + + // Set metadata. + ASSIGN_OR_RETURN( + pdpi::IrPacketIn ir_packet_in, + pdpi::PiPacketInToIr(sut_ir_p4info, packet_in.packet_in)); + *punted_output.mutable_metadata() = ir_packet_in.metadata(); + } } // Create PacketTestRuns. diff --git a/dvaas/packet_injection.h b/dvaas/packet_injection.h index 3c398be50..f2c117bb5 100644 --- a/dvaas/packet_injection.h +++ b/dvaas/packet_injection.h @@ -83,6 +83,12 @@ struct PacketInjectionParams { // The mapping of P4RT port IDs for connected interfaces between SUT and the // control switch. MirrorTestbedP4rtPortIdMap mirror_testbed_port_map; + // If false, does not collect packet-ins from SUT. Useful for scenarios where + // the connection to SUT is expected to go down such as in NSF and we don't + // want the injection/collection function to fail because of that. + // TODO: Replace with parameter to tolerate SUT disconnections + // rather than ignoring all packet-ins. + bool enable_sut_packet_in_collection = true; }; // Determines the switch's behavior when receiving test packets by: diff --git a/dvaas/traffic_generator.cc b/dvaas/traffic_generator.cc index ccb3118c0..137ef230d 100644 --- a/dvaas/traffic_generator.cc +++ b/dvaas/traffic_generator.cc @@ -160,6 +160,9 @@ void SimpleTrafficGenerator::InjectTraffic() { .mirror_testbed_port_map = params_.validation_params.mirror_testbed_port_map_override .value_or(MirrorTestbedP4rtPortIdMap::CreateIdentityMap()), + .enable_sut_packet_in_collection = + !params_.validation_params.switch_output_diff_params + .treat_expected_and_actual_outputs_as_having_no_packet_ins, }, statistics); CHECK_OK(test_runs.status()); // Crash OK. From 9bac0618f88e207776a036d3033e61ed62788bae Mon Sep 17 00:00:00 2001 From: VSuryaprasad-hcl <159443973+VSuryaprasad-HCL@users.noreply.github.com> Date: Fri, 13 Dec 2024 02:44:35 +0000 Subject: [PATCH 4/9] [Tests]: Add a packet generator which generates many fields. (#828) Co-authored-by: kishanps --- tests/lib/packet_generator.cc | 611 ++++++++++++++++++++++++++++++++++ 1 file changed, 611 insertions(+) create mode 100644 tests/lib/packet_generator.cc diff --git a/tests/lib/packet_generator.cc b/tests/lib/packet_generator.cc new file mode 100644 index 000000000..ba5e18e1a --- /dev/null +++ b/tests/lib/packet_generator.cc @@ -0,0 +1,611 @@ +// 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 "tests/lib/packet_generator.h" + +#include + +#include +#include +#include +#include + +#include "absl/numeric/int128.h" +#include "absl/random/distributions.h" +#include "absl/strings/numbers.h" +#include "absl/strings/substitute.h" +#include "gutil/proto.h" +#include "gutil/status.h" +#include "p4_pdpi/netaddr/ipv4_address.h" +#include "p4_pdpi/netaddr/mac_address.h" +#include "p4_pdpi/packetlib/bit_widths.h" +#include "p4_pdpi/packetlib/packetlib.h" +#include "p4_pdpi/packetlib/packetlib.pb.h" + +namespace pins_test { +namespace packetgen { +namespace { + +// Minimum number of TTL / HopLimit allowed for a generated packet. Any fewer +// may cause the packet to not return back to the control switch. +constexpr int kMinHops = 3; + +template +Proto ParseTextProtoOrDie(absl::string_view text) { + auto proto = gutil::ParseTextProto(text); + if (!proto.ok()) { + LOG(FATAL) << proto.status(); // Crash OK + } + return std::move(*proto); +} + +const packetlib::EthernetHeader& DefaultEthernetHeader() { + static const auto* const kHeader = new packetlib::EthernetHeader( + ParseTextProtoOrDie(R"pb( + ethernet_source: "00:00:00:00:00:7B" + ethernet_destination: "00:00:00:10:02:34" + )pb")); + return *kHeader; +} + +const packetlib::Ipv4Header& DefaultIpv4Header() { + static const auto* const kHeader = + new packetlib::Ipv4Header(ParseTextProtoOrDie(R"pb( + ihl: "0x5" + ipv4_source: "10.2.3.4" + ipv4_destination: "10.3.4.5" + ttl: "0x20" # 32 + dscp: "0x0A" + ecn: "0x0" + identification: "0x0000" + flags: "0x0" + fragment_offset: "0x0000" + )pb")); + return *kHeader; +} + +const packetlib::Ipv6Header& DefaultIpv6Header() { + static const auto* const kHeader = + new packetlib::Ipv6Header(ParseTextProtoOrDie(R"pb( + ipv6_source: "0001:0002:0003:0004::" + ipv6_destination: "0002:0003:0004:0005::" + hop_limit: "0x20" # 32 + dscp: "0x0A" + ecn: "0x0" + flow_label: "0x00000" + )pb")); + return *kHeader; +} + +const packetlib::Ipv4Header& DefaultInnerIpv4Header() { + static const auto* const kHeader = + new packetlib::Ipv4Header(ParseTextProtoOrDie(R"pb( + ihl: "0x5" + ipv4_source: "10.4.5.6" + ipv4_destination: "10.5.6.7" + ttl: "0x21" # 33 + dscp: "0x0B" + ecn: "0x0" + identification: "0x0000" + flags: "0x0" + fragment_offset: "0x0000" + )pb")); + return *kHeader; +} + +const packetlib::Ipv6Header& DefaultInnerIpv6Header() { + static const auto* const kHeader = + new packetlib::Ipv6Header(ParseTextProtoOrDie(R"pb( + ipv6_source: "0003:0004:0005:0006::" + ipv6_destination: "0004:0005:0006:0007::" + hop_limit: "0x21" # 33 + dscp: "0x0B" + ecn: "0x0" + flow_label: "0x00000" + )pb")); + return *kHeader; +} + +const packetlib::UdpHeader& DefaultUdpHeader() { + static const auto* const kHeader = + new packetlib::UdpHeader(ParseTextProtoOrDie(R"pb( + source_port: "0x0929" # 2345 + destination_port: "0x11D7" # 4567 + )pb")); + return *kHeader; +} + +packetlib::Packet DefaultIpv4Packet() { + packetlib::Packet packet; + { + packetlib::EthernetHeader l2_header = DefaultEthernetHeader(); + l2_header.set_ethertype(packetlib::EtherType(ETHERTYPE_IP)); + *packet.add_headers()->mutable_ethernet_header() = l2_header; + } + { + packetlib::Ipv4Header l3_header = DefaultIpv4Header(); + l3_header.set_protocol(packetlib::IpProtocol(IPPROTO_UDP)); + *packet.add_headers()->mutable_ipv4_header() = l3_header; + } + *packet.add_headers()->mutable_udp_header() = DefaultUdpHeader(); + return packet; +} + +packetlib::Packet DefaultIpv6Packet() { + packetlib::Packet packet; + { + packetlib::EthernetHeader l2_header = DefaultEthernetHeader(); + l2_header.set_ethertype(packetlib::EtherType(ETHERTYPE_IPV6)); + *packet.add_headers()->mutable_ethernet_header() = l2_header; + } + { + packetlib::Ipv6Header l3_header = DefaultIpv6Header(); + l3_header.set_next_header(packetlib::IpNextHeader(IPPROTO_UDP)); + *packet.add_headers()->mutable_ipv6_header() = l3_header; + } + *packet.add_headers()->mutable_udp_header() = DefaultUdpHeader(); + return packet; +} + +packetlib::Packet Default4In4Packet() { + packetlib::Packet packet; + { + packetlib::EthernetHeader l2_header = DefaultEthernetHeader(); + l2_header.set_ethertype(packetlib::EtherType(ETHERTYPE_IP)); + *packet.add_headers()->mutable_ethernet_header() = l2_header; + } + { + packetlib::Ipv4Header l3_header = DefaultIpv4Header(); + l3_header.set_protocol(packetlib::IpProtocol(IPPROTO_IPIP)); + *packet.add_headers()->mutable_ipv4_header() = l3_header; + } + { + packetlib::Ipv4Header inner_l3_header = DefaultInnerIpv4Header(); + inner_l3_header.set_protocol(packetlib::IpProtocol(IPPROTO_UDP)); + *packet.add_headers()->mutable_ipv4_header() = inner_l3_header; + } + *packet.add_headers()->mutable_udp_header() = DefaultUdpHeader(); + return packet; +} + +packetlib::Packet Default6In4Packet() { + packetlib::Packet packet; + { + packetlib::EthernetHeader l2_header = DefaultEthernetHeader(); + l2_header.set_ethertype(packetlib::EtherType(ETHERTYPE_IP)); + *packet.add_headers()->mutable_ethernet_header() = l2_header; + } + { + packetlib::Ipv4Header l3_header = DefaultIpv4Header(); + l3_header.set_protocol(packetlib::IpProtocol(IPPROTO_IPV6)); + *packet.add_headers()->mutable_ipv4_header() = l3_header; + } + { + packetlib::Ipv6Header inner_l3_header = DefaultInnerIpv6Header(); + inner_l3_header.set_next_header(packetlib::IpNextHeader(IPPROTO_UDP)); + *packet.add_headers()->mutable_ipv6_header() = inner_l3_header; + } + *packet.add_headers()->mutable_udp_header() = DefaultUdpHeader(); + return packet; +} + +packetlib::Packet Default4In6Packet() { + packetlib::Packet packet; + { + packetlib::EthernetHeader l2_header = DefaultEthernetHeader(); + l2_header.set_ethertype(packetlib::EtherType(ETHERTYPE_IPV6)); + *packet.add_headers()->mutable_ethernet_header() = l2_header; + } + { + packetlib::Ipv6Header l3_header = DefaultIpv6Header(); + l3_header.set_next_header(packetlib::IpNextHeader(IPPROTO_IPIP)); + *packet.add_headers()->mutable_ipv6_header() = l3_header; + } + { + packetlib::Ipv4Header inner_l3_header = DefaultInnerIpv4Header(); + inner_l3_header.set_protocol(packetlib::IpProtocol(IPPROTO_UDP)); + *packet.add_headers()->mutable_ipv4_header() = inner_l3_header; + } + *packet.add_headers()->mutable_udp_header() = DefaultUdpHeader(); + return packet; +} + +packetlib::Packet Default6In6Packet() { + packetlib::Packet packet; + { + packetlib::EthernetHeader l2_header = DefaultEthernetHeader(); + l2_header.set_ethertype(packetlib::EtherType(ETHERTYPE_IPV6)); + *packet.add_headers()->mutable_ethernet_header() = l2_header; + } + { + packetlib::Ipv6Header l3_header = DefaultIpv6Header(); + l3_header.set_next_header(packetlib::IpNextHeader(IPPROTO_IPV6)); + *packet.add_headers()->mutable_ipv6_header() = l3_header; + } + { + packetlib::Ipv6Header inner_l3_header = DefaultInnerIpv6Header(); + inner_l3_header.set_next_header(packetlib::IpNextHeader(IPPROTO_UDP)); + *packet.add_headers()->mutable_ipv6_header() = inner_l3_header; + } + *packet.add_headers()->mutable_udp_header() = DefaultUdpHeader(); + return packet; +} + +packetlib::Packet DefaultPacket(const Options& options) { + switch (options.ip_type) { + case IpType::kIpv4: + if (!options.inner_ip_type.has_value()) return DefaultIpv4Packet(); + switch (*options.inner_ip_type) { + case IpType::kIpv4: + return Default4In4Packet(); + case IpType::kIpv6: + return Default6In4Packet(); + } + case IpType::kIpv6: + if (!options.inner_ip_type.has_value()) return DefaultIpv6Packet(); + switch (*options.inner_ip_type) { + case IpType::kIpv4: + return Default4In6Packet(); + case IpType::kIpv6: + return Default6In6Packet(); + } + } + return packetlib::Packet(); +} + +// Header lookup for the test packet only. Assume one of the following layouts. +// * | | +// * | | | +packetlib::EthernetHeader& EthernetHeader(packetlib::Packet& packet) { + return *packet.mutable_headers(0)->mutable_ethernet_header(); +} +packetlib::Ipv4Header& Ipv4Header(packetlib::Packet& packet) { + return *packet.mutable_headers(1)->mutable_ipv4_header(); +} +packetlib::Ipv6Header& Ipv6Header(packetlib::Packet& packet) { + return *packet.mutable_headers(1)->mutable_ipv6_header(); +} +packetlib::Ipv4Header& InnerIpv4Header(packetlib::Packet& packet) { + return *packet.mutable_headers(2)->mutable_ipv4_header(); +} +packetlib::Ipv6Header& InnerIpv6Header(packetlib::Packet& packet) { + return *packet.mutable_headers(2)->mutable_ipv6_header(); +} +packetlib::UdpHeader& UdpHeader(packetlib::Packet& packet) { + return *packet.mutable_headers()->rbegin()->mutable_udp_header(); +} +IpType OuterIpHeaderType(const packetlib::Packet& packet) { + return packet.headers(1).has_ipv4_header() ? IpType::kIpv4 : IpType::kIpv6; +} +IpType InnerIpHeaderType(const packetlib::Packet& packet) { + return packet.headers(2).has_ipv4_header() ? IpType::kIpv4 : IpType::kIpv6; +} + +std::string Ipv4AddressAtIndex(int value) { + constexpr uint32_t kMinIpv4Address = 0x0a000000; // 10.0.0.0 + return netaddr::Ipv4Address(std::bitset<32>(kMinIpv4Address + value)) + .ToString(); +} +std::string Ipv6Upper64AtIndex(int value) { + constexpr uint64_t kMinIpv6Upper64 = 0x2002000000000000; // 2002:: + return netaddr::Ipv6Address(absl::MakeUint128(kMinIpv6Upper64 + value, 0)) + .ToString(); +} +std::string MacAddressAtIndex(int value) { + return netaddr::MacAddress(std::bitset<48>(value)).ToString(); +} +std::string HopLimitAtIndex(int value, IpType ip_type) { + return ip_type == IpType::kIpv4 ? packetlib::IpTtl(kMinHops + value) + : packetlib::IpHopLimit(kMinHops + value); +} + +// Set the contents of a packet field based on the given value. Depending on the +// field, a static offset may be applied to generate the contents. +// +// value is assumed to always be between [0, field range) +void SetFieldValue(Field field, int value, packetlib::Packet& packet) { + IpType ip_type = InnerIpFields().contains(field) ? InnerIpHeaderType(packet) + : OuterIpHeaderType(packet); + switch (field) { + case Field::kEthernetSrc: + EthernetHeader(packet).set_ethernet_source(MacAddressAtIndex(value)); + break; + case Field::kEthernetDst: + EthernetHeader(packet).set_ethernet_destination(MacAddressAtIndex(value)); + break; + case Field::kIpSrc: + ip_type == IpType::kIpv4 + ? Ipv4Header(packet).set_ipv4_source(Ipv4AddressAtIndex(value)) + : Ipv6Header(packet).set_ipv6_source(Ipv6Upper64AtIndex(value)); + break; + case Field::kIpDst: + ip_type == IpType::kIpv4 + ? Ipv4Header(packet).set_ipv4_destination(Ipv4AddressAtIndex(value)) + : Ipv6Header(packet).set_ipv6_destination(Ipv6Upper64AtIndex(value)); + break; + case Field::kHopLimit: + ip_type == IpType::kIpv4 + ? Ipv4Header(packet).set_ttl(HopLimitAtIndex(value, ip_type)) + : Ipv6Header(packet).set_hop_limit(HopLimitAtIndex(value, ip_type)); + break; + case Field::kDscp: + ip_type == IpType::kIpv4 + ? Ipv4Header(packet).set_dscp(packetlib::IpDscp(value)) + : Ipv6Header(packet).set_dscp(packetlib::IpDscp(value)); + break; + case Field::kFlowLabelLower16: + case Field::kFlowLabelUpper4: { + uint32_t flow_label = 0; + if (!absl::SimpleHexAtoi(Ipv6Header(packet).flow_label(), &flow_label)) { + LOG(FATAL) << "Failed to parse default flow label: '" // Crash OK + << Ipv6Header(packet).flow_label(); + } + flow_label = field == Field::kFlowLabelLower16 + ? (flow_label & ~0xffff) + value + : (flow_label & 0xffff) + (value << 16); + Ipv6Header(packet).set_flow_label(packetlib::IpFlowLabel(flow_label)); + } break; + case Field::kInnerIpSrc: + ip_type == IpType::kIpv4 + ? InnerIpv4Header(packet).set_ipv4_source(Ipv4AddressAtIndex(value)) + : InnerIpv6Header(packet).set_ipv6_source(Ipv6Upper64AtIndex(value)); + break; + case Field::kInnerIpDst: + ip_type == IpType::kIpv4 ? InnerIpv4Header(packet).set_ipv4_destination( + Ipv4AddressAtIndex(value)) + : InnerIpv6Header(packet).set_ipv6_destination( + Ipv6Upper64AtIndex(value)); + break; + case Field::kInnerHopLimit: + ip_type == IpType::kIpv4 + ? InnerIpv4Header(packet).set_ttl(HopLimitAtIndex(value, ip_type)) + : InnerIpv6Header(packet).set_hop_limit( + HopLimitAtIndex(value, ip_type)); + break; + case Field::kInnerDscp: + ip_type == IpType::kIpv4 + ? InnerIpv4Header(packet).set_dscp(packetlib::IpDscp(value)) + : InnerIpv6Header(packet).set_dscp(packetlib::IpDscp(value)); + break; + case Field::kInnerFlowLabelLower16: + case Field::kInnerFlowLabelUpper4: { + uint32_t flow_label = 0; + if (!absl::SimpleHexAtoi(InnerIpv6Header(packet).flow_label(), + &flow_label)) { + LOG(FATAL) << "Failed to parse default inner flow label: '" // Crash OK + << InnerIpv6Header(packet).flow_label() << "'"; + } + flow_label = field == Field::kInnerFlowLabelLower16 + ? (flow_label & ~0xffff) + value + : (flow_label & 0xffff) + (value << 16); + InnerIpv6Header(packet).set_flow_label( + packetlib::IpFlowLabel(flow_label)); + } break; + case Field::kL4SrcPort: + UdpHeader(packet).set_source_port(packetlib::UdpPort(value)); + break; + case Field::kL4DstPort: + UdpHeader(packet).set_destination_port(packetlib::UdpPort(value)); + break; + } +} + +int NormalizeIndex(int index) { + if (index >= 0) return index; + if (index == std::numeric_limits::min()) return 0; + return -index; +} + +int BitwidthToInt(int bitwidth) { + return bitwidth > std::numeric_limits::digits + ? std::numeric_limits::max() + : 1 << bitwidth; +} + +} // namespace + +const absl::btree_set& AllFields() { + static const auto* const kFields = new absl::btree_set({ + Field::kEthernetSrc, + Field::kEthernetDst, + Field::kIpSrc, + Field::kIpDst, + Field::kHopLimit, + Field::kDscp, + Field::kFlowLabelLower16, + Field::kFlowLabelUpper4, + Field::kInnerIpSrc, + Field::kInnerIpDst, + Field::kInnerHopLimit, + Field::kInnerDscp, + Field::kInnerFlowLabelLower16, + Field::kInnerFlowLabelUpper4, + Field::kL4SrcPort, + Field::kL4DstPort, + }); + return *kFields; +} + +std::string FieldName(Field field) { + switch (field) { + case Field::kEthernetSrc: + return "ETHERNET_SRC"; + case Field::kEthernetDst: + return "ETHERNET_DST"; + case Field::kIpSrc: + return "IP_SRC"; + case Field::kIpDst: + return "IP_DST"; + case Field::kHopLimit: + return "HOP_LIMIT"; + case Field::kDscp: + return "DSCP"; + case Field::kFlowLabelLower16: + return "FLOW_LABEL_LOWER_16"; + case Field::kFlowLabelUpper4: + return "FLOW_LABEL_UPPER_4"; + case Field::kInnerIpSrc: + return "INNER_IP_SRC"; + case Field::kInnerIpDst: + return "INNER_IP_DST"; + case Field::kInnerHopLimit: + return "INNER_HOP_LIMIT"; + case Field::kInnerDscp: + return "INNER_DSCP"; + case Field::kInnerFlowLabelLower16: + return "INNER_FLOW_LABEL_16"; + case Field::kInnerFlowLabelUpper4: + return "INNER_FLOW_LABEL_UPPER_4"; + case Field::kL4SrcPort: + return "L4_SRC_PORT"; + case Field::kL4DstPort: + return "L4_DST_PORT"; + } + return ""; +} + +const absl::btree_set& InnerIpFields() { + static const auto* const kFields = new absl::btree_set({ + Field::kInnerIpSrc, + Field::kInnerIpDst, + Field::kInnerHopLimit, + Field::kInnerDscp, + Field::kInnerFlowLabelLower16, + Field::kInnerFlowLabelUpper4, + }); + return *kFields; +} + +std::string ToString(const Options& options) { + std::string packet_type; + if (!options.inner_ip_type.has_value()) { + packet_type = options.ip_type == IpType::kIpv4 ? "IPv4" : "IPv6"; + } else { + packet_type = absl::Substitute( + "$0In$1", options.inner_ip_type == IpType::kIpv4 ? 4 : 6, + options.ip_type == IpType::kIpv4 ? 4 : 6); + } + return absl::Substitute( + "$0 Fields:{$1}", packet_type, + options.variables.empty() + ? "none" + : absl::StrJoin(options.variables, ",", + [](std::string* out, Field field) { + absl::StrAppend(out, FieldName(field)); + })); +} + +absl::Status IsValid(const Options& options) { + if (!options.inner_ip_type.has_value()) { + for (Field field : options.variables) { + if (InnerIpFields().contains(field)) { + return gutil::InvalidArgumentErrorBuilder() + << "Invalid PacketGenerator Options. Inner IP Field '" + << FieldName(field) + << "' was specified without an Inner IP type."; + } + } + } + if (options.ip_type == IpType::kIpv4) { + for (Field field : options.variables) { + if (field == Field::kFlowLabelLower16 || + field == Field::kFlowLabelUpper4) { + return gutil::InvalidArgumentErrorBuilder() + << "Invalid PacketGenerator Options. IPv6 Field '" + << FieldName(field) << "' was specified with ip_type IPv4."; + } + } + } + if (options.inner_ip_type.has_value() && + options.inner_ip_type == IpType::kIpv4) { + for (Field field : options.variables) { + if (field == Field::kInnerFlowLabelLower16 || + field == Field::kInnerFlowLabelUpper4) { + return gutil::InvalidArgumentErrorBuilder() + << "Invalid PacketGenerator Options. IPv6 Field '" + << FieldName(field) + << "' was specified with inner_ip_type IPv4."; + } + } + } + return absl::OkStatus(); +} + +packetlib::Packet PacketGenerator::Packet(int index) const { + static std::mt19937 bit_gen; + packetlib::Packet packet = DefaultPacket(options_); + packet.set_payload(Description()); + if (options_.variables.empty()) return packet; + + if (options_.variables.size() == 1) { + Field field = *options_.variables.begin(); + IpType ip_type = InnerIpFields().contains(field) ? *options_.inner_ip_type + : options_.ip_type; + SetFieldValue(field, NormalizeIndex(index) % Range(field, ip_type), packet); + return packet; + } + + bit_gen.seed(index); + for (Field field : options_.variables) { + IpType ip_type = InnerIpFields().contains(field) ? *options_.inner_ip_type + : options_.ip_type; + SetFieldValue(field, absl::Uniform(bit_gen, 0, Range(field, ip_type)), + packet); + } + return packet; +} + +std::vector PacketGenerator::Packets(int count, + int offset) const { + std::vector packets; + for (int i = offset; i < offset + count; ++i) { + packets.push_back(Packet(i)); + } + return packets; +} + +int Range(Field field, IpType ip_type) { + switch (field) { + case Field::kIpSrc: + case Field::kIpDst: + case Field::kInnerIpSrc: + case Field::kInnerIpDst: + // Reserve top prefix (8 bits for IPv4, 16 bits for IPv6). + return ip_type == IpType::kIpv4 ? BitwidthToInt(24) : BitwidthToInt(48); + case Field::kEthernetSrc: + case Field::kEthernetDst: + return BitwidthToInt(48); + case Field::kHopLimit: + case Field::kInnerHopLimit: + return ip_type == IpType::kIpv4 + ? BitwidthToInt(packetlib::kIpTtlBitwidth) - kMinHops + : BitwidthToInt(packetlib::kIpHopLimitBitwidth) - kMinHops; + case Field::kDscp: + case Field::kInnerDscp: + return BitwidthToInt(packetlib::kIpDscpBitwidth); + case Field::kFlowLabelLower16: + case Field::kInnerFlowLabelLower16: + return BitwidthToInt(16); + case Field::kFlowLabelUpper4: + case Field::kInnerFlowLabelUpper4: + return BitwidthToInt(4); + case Field::kL4SrcPort: + case Field::kL4DstPort: + return BitwidthToInt(packetlib::kUdpPortBitwidth); + } + return 0; +} + +} // namespace packetgen +} // namespace pins_test From 03990266c4cf1c59c151c11c031f6ee63d764cce Mon Sep 17 00:00:00 2001 From: VSuryaprasad-hcl <159443973+VSuryaprasad-HCL@users.noreply.github.com> Date: Fri, 13 Dec 2024 02:45:16 +0000 Subject: [PATCH 5/9] [Thinkit] Add a packet generator tests to test the generated fields. (#829) Co-authored-by: kishanps --- tests/lib/packet_generator_test.cc | 584 +++++++++++++++++++++++++++++ 1 file changed, 584 insertions(+) create mode 100644 tests/lib/packet_generator_test.cc diff --git a/tests/lib/packet_generator_test.cc b/tests/lib/packet_generator_test.cc new file mode 100644 index 000000000..df536518d --- /dev/null +++ b/tests/lib/packet_generator_test.cc @@ -0,0 +1,584 @@ +// 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 "tests/lib/packet_generator.h" + +#include + +#include +#include +#include +#include +#include +#include + +#include "absl/container/flat_hash_map.h" +#include "absl/strings/str_join.h" +#include "absl/strings/string_view.h" +#include "absl/strings/substitute.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" +#include "gutil/proto_matchers.h" +#include "gutil/status_matchers.h" +#include "p4_pdpi/packetlib/packetlib.h" +#include "p4_pdpi/packetlib/packetlib.pb.h" + +namespace pins_test { +namespace packetgen { +namespace { + +using ::gutil::EqualsProto; +using ::gutil::IsOk; +using ::testing::ElementsAre; +using ::testing::Eq; +using ::testing::Not; +using ::testing::Property; +using ::testing::ValuesIn; + +std::string OptionsTestName(const testing::TestParamInfo info) { + std::string test_name; + for (char c : ToString(info.param)) { + if (std::isalnum(c)) test_name.append(1, c); + } + return test_name; +} + +std::vector AllOptions() { + std::vector options; + for (IpType ip_type : {IpType::kIpv4, IpType::kIpv6}) { + for (Field field : AllFields()) { + for (IpType inner_ip_type : {IpType::kIpv4, IpType::kIpv6}) { + options.push_back({.ip_type = ip_type, + .variables = {field}, + .inner_ip_type = inner_ip_type}); + } + options.push_back({.ip_type = ip_type, + .variables = {field}, + .inner_ip_type = std::nullopt}); + } + } + return options; +} + +std::vector AllValidOptionsWithOneVariable() { + std::vector options = AllOptions(); + options.erase(std::remove_if(options.begin(), options.end(), + [](const Options& options) { + return !IsValid(options).ok(); + }), + options.end()); + return options; +} + +std::vector AllValidOptionsWithTwoVariables() { + // Use the name to identify options with equivalent field sets. + absl::flat_hash_map options_by_name; + for (const Options& one_var_options : AllValidOptionsWithOneVariable()) { + for (Field field : AllFields()) { + if (field == *one_var_options.variables.begin()) continue; + Options two_var_options = one_var_options; + two_var_options.variables.insert(field); + options_by_name.insert_or_assign(ToString(two_var_options), + two_var_options); + } + } + std::vector options; + for (auto& [name, option] : options_by_name) { + options.push_back(std::move(option)); + } + options.erase(std::remove_if(options.begin(), options.end(), + [](const Options& options) { + return !IsValid(options).ok(); + }), + options.end()); + return options; +} + +std::vector AllValidOptionsWithOneOrTwoVariables() { + std::vector options = AllValidOptionsWithOneVariable(); + std::vector two_var_options = AllValidOptionsWithTwoVariables(); + options.insert(options.end(), + std::make_move_iterator(two_var_options.begin()), + std::make_move_iterator(two_var_options.end())); + return options; +} + +TEST(PacketGenerator, CreateReturnsErrorForIpv6FieldsInIpv4Packet) { + EXPECT_THAT(PacketGenerator::Create({ + .ip_type = IpType::kIpv4, + .variables = {Field::kFlowLabelLower16}, + }), + Not(IsOk())); + EXPECT_THAT(PacketGenerator::Create({ + .ip_type = IpType::kIpv4, + .variables = {Field::kFlowLabelUpper4}, + }), + Not(IsOk())); +} + +TEST(PacketGenerator, CreateReturnsErrorForInnerIpv6FieldsInInnerIpv4Packet) { + EXPECT_THAT(PacketGenerator::Create({ + .ip_type = IpType::kIpv4, + .variables = {Field::kInnerFlowLabelLower16}, + .inner_ip_type = IpType::kIpv4, + }), + Not(IsOk())); + EXPECT_THAT(PacketGenerator::Create({ + .ip_type = IpType::kIpv4, + .variables = {Field::kInnerFlowLabelUpper4}, + .inner_ip_type = IpType::kIpv4, + }), + Not(IsOk())); +} + +TEST(PacketGenerator, CreateReturnsErrorForInnerIpFieldInUnnestedPacket) { + EXPECT_THAT(PacketGenerator::Create({ + .ip_type = IpType::kIpv4, + .variables = {Field::kInnerIpSrc}, + }), + Not(IsOk())); + EXPECT_THAT(PacketGenerator::Create({ + .ip_type = IpType::kIpv4, + .variables = {Field::kInnerIpDst}, + }), + Not(IsOk())); + EXPECT_THAT(PacketGenerator::Create({ + .ip_type = IpType::kIpv4, + .variables = {Field::kInnerHopLimit}, + }), + Not(IsOk())); + EXPECT_THAT(PacketGenerator::Create({ + .ip_type = IpType::kIpv4, + .variables = {Field::kInnerDscp}, + }), + Not(IsOk())); + EXPECT_THAT(PacketGenerator::Create({ + .ip_type = IpType::kIpv4, + .variables = {Field::kInnerFlowLabelLower16}, + }), + Not(IsOk())); + EXPECT_THAT(PacketGenerator::Create({ + .ip_type = IpType::kIpv4, + .variables = {Field::kInnerFlowLabelUpper4}, + }), + Not(IsOk())); +} + +TEST(PacketGenerator, CreateReturnsErrorForAnyVariableMismatch) { + EXPECT_THAT(PacketGenerator::Create({ + .ip_type = IpType::kIpv4, + .variables = {Field::kIpSrc, Field::kInnerFlowLabelUpper4}, + }), + Not(IsOk())); +} + +TEST(PacketGenerator, GeneratesValidIpv4Packet) { + ASSERT_OK_AND_ASSIGN(PacketGenerator generator, + PacketGenerator::Create({.ip_type = IpType::kIpv4})); + EXPECT_OK(packetlib::SerializePacket(generator.Packet())); + EXPECT_THAT( + generator.Packet().headers(), + ElementsAre(Property(&packetlib::Header::has_ethernet_header, true), + Property(&packetlib::Header::has_ipv4_header, true), + Property(&packetlib::Header::has_udp_header, true))); +} + +TEST(PacketGenerator, GeneratesValidIpv6Packet) { + ASSERT_OK_AND_ASSIGN(PacketGenerator generator, + PacketGenerator::Create({.ip_type = IpType::kIpv6})); + EXPECT_OK(packetlib::SerializePacket(generator.Packet())); + EXPECT_THAT( + generator.Packet().headers(), + ElementsAre(Property(&packetlib::Header::has_ethernet_header, true), + Property(&packetlib::Header::has_ipv6_header, true), + Property(&packetlib::Header::has_udp_header, true))); +} + +TEST(PacketGenerator, GeneratesValid4In4Packet) { + ASSERT_OK_AND_ASSIGN(PacketGenerator generator, + PacketGenerator::Create({ + .ip_type = IpType::kIpv4, + .inner_ip_type = IpType::kIpv4, + })); + EXPECT_OK(packetlib::SerializePacket(generator.Packet())); + EXPECT_THAT( + generator.Packet().headers(), + ElementsAre(Property(&packetlib::Header::has_ethernet_header, true), + Property(&packetlib::Header::has_ipv4_header, true), + Property(&packetlib::Header::has_ipv4_header, true), + Property(&packetlib::Header::has_udp_header, true))); +} + +TEST(PacketGenerator, GeneratesValid6In4Packet) { + ASSERT_OK_AND_ASSIGN(PacketGenerator generator, + PacketGenerator::Create({ + .ip_type = IpType::kIpv4, + .inner_ip_type = IpType::kIpv6, + })); + EXPECT_OK(packetlib::SerializePacket(generator.Packet())); + EXPECT_THAT( + generator.Packet().headers(), + ElementsAre(Property(&packetlib::Header::has_ethernet_header, true), + Property(&packetlib::Header::has_ipv4_header, true), + Property(&packetlib::Header::has_ipv6_header, true), + Property(&packetlib::Header::has_udp_header, true))); +} + +TEST(PacketGenerator, GeneratesValid4In6Packet) { + ASSERT_OK_AND_ASSIGN(PacketGenerator generator, + PacketGenerator::Create({ + .ip_type = IpType::kIpv6, + .inner_ip_type = IpType::kIpv4, + })); + EXPECT_OK(packetlib::SerializePacket(generator.Packet())); + EXPECT_THAT( + generator.Packet().headers(), + ElementsAre(Property(&packetlib::Header::has_ethernet_header, true), + Property(&packetlib::Header::has_ipv6_header, true), + Property(&packetlib::Header::has_ipv4_header, true), + Property(&packetlib::Header::has_udp_header, true))); +} + +TEST(PacketGenerator, GeneratesValid6In6Packet) { + ASSERT_OK_AND_ASSIGN(PacketGenerator generator, + PacketGenerator::Create({ + .ip_type = IpType::kIpv6, + .inner_ip_type = IpType::kIpv6, + })); + EXPECT_OK(packetlib::SerializePacket(generator.Packet())); + EXPECT_THAT( + generator.Packet().headers(), + ElementsAre(Property(&packetlib::Header::has_ethernet_header, true), + Property(&packetlib::Header::has_ipv6_header, true), + Property(&packetlib::Header::has_ipv6_header, true), + Property(&packetlib::Header::has_udp_header, true))); +} + +TEST(PacketGenerator, GeneratesMultipleFields) { + ASSERT_OK_AND_ASSIGN( + PacketGenerator generator, + PacketGenerator::Create({ + .ip_type = IpType::kIpv4, + .variables = {Field::kIpSrc, Field::kIpDst, Field::kL4DstPort}, + })); + packetlib::Packet packet0 = generator.Packet(0); + packetlib::Packet static_packet0 = packet0; + static_packet0.mutable_headers(1)->mutable_ipv4_header()->clear_ipv4_source(); + static_packet0.mutable_headers(1) + ->mutable_ipv4_header() + ->clear_ipv4_destination(); + static_packet0.mutable_headers(2) + ->mutable_udp_header() + ->clear_destination_port(); + + packetlib::Packet packet1 = generator.Packet(1); + packetlib::Packet static_packet1 = packet1; + static_packet1.mutable_headers(1)->mutable_ipv4_header()->clear_ipv4_source(); + static_packet1.mutable_headers(1) + ->mutable_ipv4_header() + ->clear_ipv4_destination(); + static_packet1.mutable_headers(2) + ->mutable_udp_header() + ->clear_destination_port(); + + SCOPED_TRACE( + absl::Substitute("Failed to verify packet diff from packets generator $0", + generator.Description())); + EXPECT_THAT(static_packet0, EqualsProto(static_packet1)); + EXPECT_THAT(packet0.headers(1).ipv4_header().ipv4_source(), + Not(Eq(packet1.headers(1).ipv4_header().ipv4_source()))); + EXPECT_THAT(packet0.headers(1).ipv4_header().ipv4_destination(), + Not(Eq(packet1.headers(1).ipv4_header().ipv4_destination()))); + EXPECT_THAT(packet0.headers(2).udp_header().destination_port(), + Not(Eq(packet1.headers(2).udp_header().destination_port()))); +} + +// Define a tuple-based matcher for EqualsProto for use in testing::Pointwise. +MATCHER(PointwiseEqualsProto, "") { + return gutil::ProtobufEqMatcher(std::get<1>(arg)) + .MatchAndExplain(std::get<0>(arg), result_listener); +} + +TEST(PacketGenerator, PacketsMatchesIndividualPacketResults) { + ASSERT_OK_AND_ASSIGN(PacketGenerator generator, + PacketGenerator::Create({ + .ip_type = IpType::kIpv4, + .variables = {Field::kIpSrc, Field::kL4SrcPort}, + .inner_ip_type = IpType::kIpv6, + })); + std::vector packets = generator.Packets(10); + std::vector individual_packets; + for (int i = 0; i < 10; ++i) { + individual_packets.push_back(generator.Packet(i)); + } + EXPECT_THAT(packets, + testing::Pointwise(PointwiseEqualsProto(), individual_packets)); +} + +TEST(PacketGenerator, PacketsMatchesIndividualPacketResultsWithOffset) { + ASSERT_OK_AND_ASSIGN(PacketGenerator generator, + PacketGenerator::Create({ + .ip_type = IpType::kIpv4, + .variables = {Field::kIpSrc, Field::kL4SrcPort}, + .inner_ip_type = IpType::kIpv6, + })); + std::vector packets = generator.Packets(10, 11); + std::vector individual_packets; + for (int i = 11; i < 11 + 10; ++i) { + individual_packets.push_back(generator.Packet(i)); + } + EXPECT_THAT(packets, + testing::Pointwise(PointwiseEqualsProto(), individual_packets)); +} + +class PacketGeneratorOptions : public testing::TestWithParam {}; + +TEST_P(PacketGeneratorOptions, AreAllValidOrInvalid) { + auto generator = PacketGenerator::Create(GetParam()); + if (generator.ok()) { + EXPECT_OK(packetlib::SerializePacket(generator->Packet())); + } +} + +INSTANTIATE_TEST_SUITE_P(AllOptions, PacketGeneratorOptions, + ValuesIn(AllOptions()), OptionsTestName); + +class SingleFieldOptions : public testing::TestWithParam {}; + +TEST_P(SingleFieldOptions, RangeIsPositive) { + Field field = *GetParam().variables.begin(); + switch (field) { + case Field::kFlowLabelLower16: + case Field::kFlowLabelUpper4: + case Field::kInnerFlowLabelLower16: + case Field::kInnerFlowLabelUpper4: + EXPECT_GT(Range(field, IpType::kIpv4), 0); + break; + default: + EXPECT_GT(Range(field, IpType::kIpv4), 0); + EXPECT_GT(Range(field, IpType::kIpv6), 0); + } +} + +TEST_P(SingleFieldOptions, EditsOnlyTheRequestedField) { + ASSERT_OK_AND_ASSIGN(auto generator, PacketGenerator::Create(GetParam())); + packetlib::Packet packet0 = generator.Packet(); + packet0.clear_payload(); + packetlib::Packet packet1 = generator.Packet(1); + packet1.clear_payload(); + + bool encapped = GetParam().inner_ip_type.has_value(); + switch (*GetParam().variables.begin()) { + case Field::kEthernetSrc: + packet0.mutable_headers(0) + ->mutable_ethernet_header() + ->clear_ethernet_source(); + packet1.mutable_headers(0) + ->mutable_ethernet_header() + ->clear_ethernet_source(); + break; + case Field::kEthernetDst: + packet0.mutable_headers(0) + ->mutable_ethernet_header() + ->clear_ethernet_destination(); + packet1.mutable_headers(0) + ->mutable_ethernet_header() + ->clear_ethernet_destination(); + break; + case Field::kIpSrc: + if (GetParam().ip_type == IpType::kIpv4) { + packet0.mutable_headers(1)->mutable_ipv4_header()->clear_ipv4_source(); + packet1.mutable_headers(1)->mutable_ipv4_header()->clear_ipv4_source(); + } else { + packet0.mutable_headers(1)->mutable_ipv6_header()->clear_ipv6_source(); + packet1.mutable_headers(1)->mutable_ipv6_header()->clear_ipv6_source(); + } + break; + case Field::kIpDst: + if (GetParam().ip_type == IpType::kIpv4) { + packet0.mutable_headers(1) + ->mutable_ipv4_header() + ->clear_ipv4_destination(); + packet1.mutable_headers(1) + ->mutable_ipv4_header() + ->clear_ipv4_destination(); + } else { + packet0.mutable_headers(1) + ->mutable_ipv6_header() + ->clear_ipv6_destination(); + packet1.mutable_headers(1) + ->mutable_ipv6_header() + ->clear_ipv6_destination(); + } + break; + case Field::kHopLimit: + if (GetParam().ip_type == IpType::kIpv4) { + packet0.mutable_headers(1)->mutable_ipv4_header()->clear_ttl(); + packet1.mutable_headers(1)->mutable_ipv4_header()->clear_ttl(); + } else { + packet0.mutable_headers(1)->mutable_ipv6_header()->clear_hop_limit(); + packet1.mutable_headers(1)->mutable_ipv6_header()->clear_hop_limit(); + } + break; + case Field::kDscp: + if (GetParam().ip_type == IpType::kIpv4) { + packet0.mutable_headers(1)->mutable_ipv4_header()->clear_dscp(); + packet1.mutable_headers(1)->mutable_ipv4_header()->clear_dscp(); + } else { + packet0.mutable_headers(1)->mutable_ipv6_header()->clear_dscp(); + packet1.mutable_headers(1)->mutable_ipv6_header()->clear_dscp(); + } + break; + // Flow label is 20 bits, which is 5 hex digits +2 chars for '0x'. + // We split the hex string into: + // * Upper-4 bits (0xN)nnnn | chars [0 - 2] + // * Lower-16 bits 0xn(NNNN) | chars [3 - 6] + case Field::kFlowLabelLower16: + EXPECT_THAT( + packet0.headers(1).ipv6_header().flow_label().substr(0, 3), + Eq(packet1.headers(1).ipv6_header().flow_label().substr(0, 3))) + << "Unexpected difference in upper 4 bits of Flow Label."; + packet0.mutable_headers(1)->mutable_ipv6_header()->clear_flow_label(); + packet1.mutable_headers(1)->mutable_ipv6_header()->clear_flow_label(); + break; + case Field::kFlowLabelUpper4: + EXPECT_THAT(packet0.headers(1).ipv6_header().flow_label().substr(3), + Eq(packet1.headers(1).ipv6_header().flow_label().substr(3))) + << "Unexpected difference in lower 16 bits of Flow Label."; + packet0.mutable_headers(1)->mutable_ipv6_header()->clear_flow_label(); + packet1.mutable_headers(1)->mutable_ipv6_header()->clear_flow_label(); + break; + case Field::kInnerIpSrc: + if (GetParam().inner_ip_type == IpType::kIpv4) { + packet0.mutable_headers(2)->mutable_ipv4_header()->clear_ipv4_source(); + packet1.mutable_headers(2)->mutable_ipv4_header()->clear_ipv4_source(); + } else { + packet0.mutable_headers(2)->mutable_ipv6_header()->clear_ipv6_source(); + packet1.mutable_headers(2)->mutable_ipv6_header()->clear_ipv6_source(); + } + break; + case Field::kInnerIpDst: + if (GetParam().inner_ip_type == IpType::kIpv4) { + packet0.mutable_headers(2) + ->mutable_ipv4_header() + ->clear_ipv4_destination(); + packet1.mutable_headers(2) + ->mutable_ipv4_header() + ->clear_ipv4_destination(); + } else { + packet0.mutable_headers(2) + ->mutable_ipv6_header() + ->clear_ipv6_destination(); + packet1.mutable_headers(2) + ->mutable_ipv6_header() + ->clear_ipv6_destination(); + } + break; + case Field::kInnerHopLimit: + if (GetParam().inner_ip_type == IpType::kIpv4) { + packet0.mutable_headers(2)->mutable_ipv4_header()->clear_ttl(); + packet1.mutable_headers(2)->mutable_ipv4_header()->clear_ttl(); + } else { + packet0.mutable_headers(2)->mutable_ipv6_header()->clear_hop_limit(); + packet1.mutable_headers(2)->mutable_ipv6_header()->clear_hop_limit(); + } + break; + case Field::kInnerDscp: + if (GetParam().inner_ip_type == IpType::kIpv4) { + packet0.mutable_headers(2)->mutable_ipv4_header()->clear_dscp(); + packet1.mutable_headers(2)->mutable_ipv4_header()->clear_dscp(); + } else { + packet0.mutable_headers(2)->mutable_ipv6_header()->clear_dscp(); + packet1.mutable_headers(2)->mutable_ipv6_header()->clear_dscp(); + } + break; + case Field::kInnerFlowLabelLower16: + EXPECT_THAT( + packet0.headers(2).ipv6_header().flow_label().substr(0, 3), + Eq(packet1.headers(2).ipv6_header().flow_label().substr(0, 3))) + << "Unexpected difference in upper 4 bits of Flow Label."; + packet0.mutable_headers(2)->mutable_ipv6_header()->clear_flow_label(); + packet1.mutable_headers(2)->mutable_ipv6_header()->clear_flow_label(); + break; + case Field::kInnerFlowLabelUpper4: + EXPECT_THAT(packet0.headers(2).ipv6_header().flow_label().substr(3), + Eq(packet1.headers(2).ipv6_header().flow_label().substr(3))) + << "Unexpected difference in lower 16 bits of Flow Label."; + packet0.mutable_headers(2)->mutable_ipv6_header()->clear_flow_label(); + packet1.mutable_headers(2)->mutable_ipv6_header()->clear_flow_label(); + break; + case Field::kL4SrcPort: + packet0.mutable_headers(encapped ? 3 : 2) + ->mutable_udp_header() + ->clear_source_port(); + packet1.mutable_headers(encapped ? 3 : 2) + ->mutable_udp_header() + ->clear_source_port(); + break; + case Field::kL4DstPort: + packet0.mutable_headers(encapped ? 3 : 2) + ->mutable_udp_header() + ->clear_destination_port(); + packet1.mutable_headers(encapped ? 3 : 2) + ->mutable_udp_header() + ->clear_destination_port(); + break; + } + EXPECT_THAT(packet0, EqualsProto(packet1)); +} + +INSTANTIATE_TEST_SUITE_P(PacketGeneratorTest, SingleFieldOptions, + ValuesIn(AllValidOptionsWithOneVariable()), + OptionsTestName); + +class SingleOrDoubleFieldOptions : public testing::TestWithParam {}; + +TEST_P(SingleOrDoubleFieldOptions, CreatesDifferentPackets) { + ASSERT_OK_AND_ASSIGN(auto generator, PacketGenerator::Create(GetParam())); + std::vector packet_descriptions; + std::set packet_contents; + constexpr int kNumPackets = 4; + for (int i = 0; i < kNumPackets; ++i) { + packetlib::Packet packet = generator.Packet(i); + packet.set_payload(""); // Only check the header. + packet_descriptions.push_back(packet.ShortDebugString()); + ASSERT_OK_AND_ASSIGN(std::string raw_packet, + packetlib::SerializePacket(packet)); + packet_contents.insert(std::move(raw_packet)); + } + EXPECT_EQ(packet_contents.size(), kNumPackets) + << "Expected packets: " << absl::StrJoin(packet_descriptions, "\n"); +} + +TEST_P(SingleOrDoubleFieldOptions, GeneratesAValidPacketAtAnyIndex) { + ASSERT_OK_AND_ASSIGN(auto generator, PacketGenerator::Create(GetParam())); + EXPECT_OK(SerializePacket(generator.Packet(0))); + for (int i = 0; i < std::numeric_limits::digits; ++i) { + int index = 1 << i; + EXPECT_OK(SerializePacket(generator.Packet(index))); + EXPECT_OK(SerializePacket(generator.Packet(index - 1))); + EXPECT_OK(SerializePacket(generator.Packet(-index))); + EXPECT_OK(SerializePacket(generator.Packet(-index + 1))); + } + EXPECT_OK(SerializePacket(generator.Packet(std::numeric_limits::max()))); + EXPECT_OK(SerializePacket(generator.Packet(std::numeric_limits::min()))); +} + +INSTANTIATE_TEST_SUITE_P(PacketGeneratorTest, SingleOrDoubleFieldOptions, + ValuesIn(AllValidOptionsWithOneOrTwoVariables()), + OptionsTestName); + +} // namespace +} // namespace packetgen +} // namespace pins_test From 582aed66d56d6fb38f3a8c7919c11aaa86e23e94 Mon Sep 17 00:00:00 2001 From: VSuryaprasad-hcl <159443973+VSuryaprasad-HCL@users.noreply.github.com> Date: Fri, 13 Dec 2024 02:46:04 +0000 Subject: [PATCH 6/9] [Thinkit] Add a packet generator headers which generates many fields. (#830) Co-authored-by: kishanps --- tests/lib/BUILD.bazel | 37 +++++++++++ tests/lib/packet_generator.h | 115 +++++++++++++++++++++++++++++++++++ 2 files changed, 152 insertions(+) create mode 100644 tests/lib/packet_generator.h diff --git a/tests/lib/BUILD.bazel b/tests/lib/BUILD.bazel index 130c8c4f6..fe5d7e207 100644 --- a/tests/lib/BUILD.bazel +++ b/tests/lib/BUILD.bazel @@ -141,3 +141,40 @@ cc_library( "@com_google_absl//absl/synchronization", ], ) + +cc_library( + name = "packet_generator", + testonly = True, + srcs = ["packet_generator.cc"], + hdrs = ["packet_generator.h"], + deps = [ + "//gutil:proto", + "//gutil:status", + "//p4_pdpi/netaddr:ipv4_address", + "//p4_pdpi/netaddr:mac_address", + "//p4_pdpi/packetlib", + "//p4_pdpi/packetlib:bit_widths", + "//p4_pdpi/packetlib:packetlib_cc_proto", + "@com_google_absl//absl/container:btree", + "@com_google_absl//absl/numeric:int128", + "@com_google_absl//absl/random:distributions", + "@com_google_absl//absl/status:statusor", + "@com_google_absl//absl/strings", + "@com_google_absl//absl/strings:str_format", + ], +) + +cc_test( + name = "packet_generator_test", + srcs = ["packet_generator_test.cc"], + deps = [ + ":packet_generator", + "//gutil:proto_matchers", + "//gutil:status_matchers", + "//p4_pdpi/packetlib", + "//p4_pdpi/packetlib:packetlib_cc_proto", + "@com_google_absl//absl/container:flat_hash_map", + "@com_google_absl//absl/strings", + "@com_google_googletest//:gtest_main", + ], +) diff --git a/tests/lib/packet_generator.h b/tests/lib/packet_generator.h new file mode 100644 index 000000000..bc1e72da8 --- /dev/null +++ b/tests/lib/packet_generator.h @@ -0,0 +1,115 @@ +// 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_TESTS_LIB_PACKET_GENERATOR_H_ +#define PINS_TESTS_LIB_PACKET_GENERATOR_H_ + +#include +#include + +#include +#include + +#include "absl/container/btree_set.h" +#include "absl/status/statusor.h" +#include "absl/strings/str_format.h" +#include "absl/strings/str_join.h" +#include "gutil/status.h" // IWYU pragma: keep +#include "p4_pdpi/packetlib/packetlib.pb.h" + +// Helper library to hold a collection of functions to define a test +// configuration, define a packet, generate a packet etc. +namespace pins_test { +namespace packetgen { + +// Modifiable fields within a packet. +enum class Field { + kEthernetSrc, + kEthernetDst, + kIpSrc, + kIpDst, + kHopLimit, + kDscp, + kFlowLabelLower16, + kFlowLabelUpper4, + kInnerIpSrc, + kInnerIpDst, + kInnerHopLimit, + kInnerDscp, + kInnerFlowLabelLower16, + kInnerFlowLabelUpper4, + kL4SrcPort, + kL4DstPort, + // Any new Field values must be added to AllFields(). +}; + +// Returns the string name of a field. +std::string FieldName(Field field); + +// Returns a list of all field enums. +const absl::btree_set& AllFields(); +const absl::btree_set& InnerIpFields(); + +enum class IpType { kIpv4, kIpv6 }; + +// Options to define the packet generation behavior. +struct Options { + IpType ip_type; // IP type of the packet or outer IP type if encapped. + absl::btree_set variables; // Set of fields to vary. + std::optional inner_ip_type; // Inner IP type. Required if encapped. +}; +inline Options Ipv4PacketOptions() { return {.ip_type = IpType::kIpv4}; } +inline Options Ipv6PacketOptions() { return {.ip_type = IpType::kIpv6}; } + +// Returns ok if the Options struct is valid or an error if it is invalid. +absl::Status IsValid(const Options& options); + +// Returns a string describing the Options struct. +std::string ToString(const Options& options); + +// Returns the number of unique values that can be generated for the field. +int Range(Field field, IpType ip_type); + +// This class generates packets from the provided options. Once the packet +// generator is created, it is expected that any Packet() call will create a +// valid packet. +class PacketGenerator { + public: + // Factory function. + static absl::StatusOr Create(Options options) { + RETURN_IF_ERROR(IsValid(options)); + return PacketGenerator(std::move(options)); + } + + // Returns a description of the generator options. + std::string Description() const { return ToString(options_); } + + // Returns a packet at the given index. Subsequent calls for the same index + // will return the same packet. + packetlib::Packet Packet(int index = 0) const; + + // Returns multiple packets with sequential indices. An offset may be given to + // change the starting index. + std::vector Packets(int count, int offset = 0) const; + + private: + explicit PacketGenerator(Options options) : options_(std::move(options)) {} + + const Options options_; +}; + +} // namespace packetgen +} // namespace pins_test + +#endif // PINS_TESTS_LIB_PACKET_GENERATOR_H_ From b6e267b9e33b96e701514a9fecbc140056777538 Mon Sep 17 00:00:00 2001 From: VSuryaprasad-hcl <159443973+VSuryaprasad-HCL@users.noreply.github.com> Date: Fri, 13 Dec 2024 02:46:53 +0000 Subject: [PATCH 7/9] [P4_Symbolic] Modified p4_symbolic/symbolic/parser_test.cc [Evaluate parsers symbolically.] (#831) Co-authored-by: kishanps --- p4_symbolic/symbolic/parser_test.cc | 680 ++++++++++++++++++++++++++++ 1 file changed, 680 insertions(+) diff --git a/p4_symbolic/symbolic/parser_test.cc b/p4_symbolic/symbolic/parser_test.cc index de18af822..b9ff9bbdb 100644 --- a/p4_symbolic/symbolic/parser_test.cc +++ b/p4_symbolic/symbolic/parser_test.cc @@ -312,5 +312,685 @@ constexpr absl::string_view kProgramExtractEthernet = R"pb( } )pb"; +TEST(EvaluateParsers, ReturnsErrorForNonFreeBitVectorHeaderField) { + ASSERT_OK_AND_ASSIGN(const ir::Dataplane data_plane, + ParseProgramTextProto(kProgramExtractEthernet)); + ASSERT_OK_AND_ASSIGN(SymbolicPerPacketState ingress_headers, + SymbolicGuardedMap::CreateSymbolicGuardedMap( + data_plane.program.headers())); + ASSERT_OK(ingress_headers.Set("ethernet.dst_addr", Z3Context().bv_val(0, 48), + Z3Context().bool_val(true))); + EXPECT_THAT(EvaluateParsers(data_plane.program, ingress_headers), + StatusIs(absl::StatusCode::kInvalidArgument, + "Field 'ethernet.dst_addr' should be a free bit-vector. " + "Found: (ite true #x000000000000 ethernet.dst_addr)")); +} + +constexpr absl::string_view kProgramWithUnknownFieldSet = R"pb( + parsers { + key: "parser" + value { + name: "parser" + initial_state: "start" + parse_states { + key: "start" + value { + name: "start" + parser_ops { + set { + lvalue { header_name: "ethernet" field_name: "unknown" } + hexstr_rvalue { value: "0xBEEF" } + } + } + transitions { default_transition { next_state: $eoparser } } + } + } + } + } +)pb"; + +TEST(EvaluateParsers, ReturnsErrorForUnknownFieldSet) { + ASSERT_OK_AND_ASSIGN(const ir::Dataplane data_plane, + ParseProgramTextProto(kProgramWithUnknownFieldSet)); + ASSERT_OK_AND_ASSIGN(SymbolicPerPacketState ingress_headers, + SymbolicGuardedMap::CreateSymbolicGuardedMap( + data_plane.program.headers())); + EXPECT_THAT( + EvaluateParsers(data_plane.program, ingress_headers), + StatusIs( + absl::StatusCode::kInvalidArgument, + "Cannot assign to key \"ethernet.unknown\" in SymbolicGuardedMap!")); +} + +constexpr absl::string_view kProgramWithLookaheadSet = R"pb( + parsers { + key: "parser" + value { + name: "parser" + initial_state: "start" + parse_states { + key: "start" + value { + name: "start" + parser_ops { + set { + lvalue { header_name: "ethernet" field_name: "dst_addr" } + lookahead_rvalue { bitwidth: 48 } + } + } + transitions { default_transition { next_state: $eoparser } } + } + } + } + } +)pb"; + +TEST(EvaluateParsers, ReturnsErrorForUnimplementedLookaheadSet) { + ASSERT_OK_AND_ASSIGN(const ir::Dataplane data_plane, + ParseProgramTextProto(kProgramWithLookaheadSet)); + ASSERT_OK_AND_ASSIGN(SymbolicPerPacketState ingress_headers, + SymbolicGuardedMap::CreateSymbolicGuardedMap( + data_plane.program.headers())); + EXPECT_THAT( + EvaluateParsers(data_plane.program, ingress_headers), + StatusIs(absl::StatusCode::kUnimplemented, + "Lookahead R-values for set operations are not supported.")); +} + +constexpr absl::string_view kProgramWithNonHeaderFieldTransitionKey = R"pb( + parsers { + key: "parser" + value { + name: "parser" + initial_state: "start" + parse_states { + key: "start" + value { + name: "start" + transition_key_fields { lookahead { bitwidth: 16 } } + transitions { default_transition { next_state: $eoparser } } + } + } + } + } +)pb"; + +TEST(EvaluateParsers, ReturnsErrorForNonHeaderFieldTransitionKey) { + ASSERT_OK_AND_ASSIGN( + const ir::Dataplane data_plane, + ParseProgramTextProto(kProgramWithNonHeaderFieldTransitionKey)); + ASSERT_OK_AND_ASSIGN(SymbolicPerPacketState ingress_headers, + SymbolicGuardedMap::CreateSymbolicGuardedMap( + data_plane.program.headers())); + EXPECT_THAT(EvaluateParsers(data_plane.program, ingress_headers), + StatusIs(absl::StatusCode::kUnimplemented)); +} + +constexpr absl::string_view kProgramWithUnknownFieldTransitionKey = R"pb( + parsers { + key: "parser" + value { + name: "parser" + initial_state: "start" + parse_states { + key: "start" + value { + name: "start" + transition_key_fields { + field { header_name: "un" field_name: "known" } + } + transitions { default_transition { next_state: $eoparser } } + } + } + } + } +)pb"; + +TEST(EvaluateParsers, ReturnsErrorForUnknownFieldTransitionKey) { + ASSERT_OK_AND_ASSIGN( + const ir::Dataplane data_plane, + ParseProgramTextProto(kProgramWithUnknownFieldTransitionKey)); + ASSERT_OK_AND_ASSIGN(SymbolicPerPacketState ingress_headers, + SymbolicGuardedMap::CreateSymbolicGuardedMap( + data_plane.program.headers())); + EXPECT_THAT(EvaluateParsers(data_plane.program, ingress_headers), + StatusIs(absl::StatusCode::kInvalidArgument, + "Cannot find key \"un.known\" in SymbolicGuardedMap!")); +} + +constexpr absl::string_view kProgramWithDeadCodeTransition = R"pb( + parsers { + key: "parser" + value { + name: "parser" + initial_state: "start" + parse_states { + key: "start" + value { + name: "start" + transition_key_fields { + field { header_name: "ethernet" field_name: "ether_type" } + } + transitions { + hex_string_transition { + value { value: "0x0000" } + mask { value: "0xfe00" } + next_state: $eoparser + } + } + transitions { default_transition { next_state: $eoparser } } + transitions { + hex_string_transition { + value { value: "0x0800" } + mask {} + next_state: "parse_ipv4" + } + } + } + } + } + } +)pb"; + +TEST(EvaluateParsers, ReturnsErrorForDeadCodeTransition) { + ASSERT_OK_AND_ASSIGN(const ir::Dataplane data_plane, + ParseProgramTextProto(kProgramWithDeadCodeTransition)); + ASSERT_OK_AND_ASSIGN(SymbolicPerPacketState ingress_headers, + SymbolicGuardedMap::CreateSymbolicGuardedMap( + data_plane.program.headers())); + EXPECT_THAT(EvaluateParsers(data_plane.program, ingress_headers), + StatusIs(absl::StatusCode::kInvalidArgument)); +} + +constexpr absl::string_view kProgramWithNoTransitionKey = R"pb( + parsers { + key: "parser" + value { + name: "parser" + initial_state: "start" + parse_states { + key: "start" + value { + name: "start" + transitions { + hex_string_transition { + value { value: "0x0800" } + mask {} + next_state: "parse_ipv4" + } + } + transitions { default_transition { next_state: $eoparser } } + } + } + } + } +)pb"; + +TEST(EvaluateParsers, ReturnsErrorForNoTransitionKey) { + ASSERT_OK_AND_ASSIGN(const ir::Dataplane data_plane, + ParseProgramTextProto(kProgramWithNoTransitionKey)); + ASSERT_OK_AND_ASSIGN(SymbolicPerPacketState ingress_headers, + SymbolicGuardedMap::CreateSymbolicGuardedMap( + data_plane.program.headers())); + EXPECT_THAT( + EvaluateParsers(data_plane.program, ingress_headers), + StatusIs( + absl::StatusCode::kNotFound, + "No transition key specified but hex string transitions exist.")); +} + +struct ParserTestParam { + // Text proto representing the input IR P4Program. + std::string ir_program_text_proto; + // Expected SMT formulae of the parsed headers keyed with fully qualified + // header field names. If a header field is not specified in this map, its + // symbolic value remains untouched, meaning that it will be the same as that + // of the ingress headers. Note that the symbolic expressions of all header + // fields are checked against the parsed headers rather than just the + // specified ones. + std::function(z3::context&)> + expected_symbolic_parsed_headers; +}; + +using EvaluateParsersTest = ::testing::TestWithParam; + +std::vector GetParserTestInstances() { + return { + { + // Minimal parser. The parsed headers should be the same as the + // ingress headers. + .ir_program_text_proto = R"pb( + parsers { + key: "parser" + value { + name: "parser" + initial_state: "start" + parse_states { + key: "start" + value { + name: "start" + transitions { default_transition { next_state: $eoparser } } + optimized_symbolic_execution_info { + merge_point: $eoparser + continue_to_merge_point: true + } + } + } + } + } + )pb", + .expected_symbolic_parsed_headers = [](z3::context& ctx) + -> absl::btree_map { return {}; }, + }, + { + // Basic parser with Ethernet and IPv4 headers. + .ir_program_text_proto = R"pb( + parsers { + key: "parser" + value { + name: "parser" + initial_state: "start" + parse_states { + key: "start" + value { + name: "start" + parser_ops { + extract { header { header_name: "ethernet" } } + } + transition_key_fields { + field { header_name: "ethernet" field_name: "ether_type" } + } + transitions { + hex_string_transition { + value { value: "0x0800" } + mask {} + next_state: "parse_ipv4" + } + } + transitions { default_transition { next_state: $eoparser } } + optimized_symbolic_execution_info { + merge_point: $eoparser + continue_to_merge_point: true + } + } + } + parse_states { + key: "parse_ipv4" + value { + name: "parse_ipv4" + parser_ops { extract { header { header_name: "ipv4" } } } + transitions { default_transition { next_state: $eoparser } } + optimized_symbolic_execution_info { merge_point: $eoparser } + } + } + } + } + )pb", + .expected_symbolic_parsed_headers = + [](z3::context& ctx) -> absl::btree_map { + return { + {"ethernet.$valid$", ctx.bool_val(true)}, + {"ethernet.$extracted$", ctx.bool_val(true)}, + {"ipv4.$valid$", (ctx.bv_const("ethernet.ether_type", 16) == + ctx.bv_val(0x0800, 16))}, + {"ipv4.$extracted$", (ctx.bv_const("ethernet.ether_type", 16) == + ctx.bv_val(0x0800, 16))}, + }; + }, + }, + { + // Parser with set operations. + .ir_program_text_proto = R"pb( + parsers { + key: "parser" + value { + name: "parser" + initial_state: "start" + parse_states { + key: "start" + value { + name: "start" + parser_ops { + extract { header { header_name: "ethernet" } } + } + parser_ops { + set { + lvalue { + header_name: "ethernet" + field_name: "dst_addr" + } + hexstr_rvalue { value: "0x0000DEADBEEF" } + } + } + parser_ops { + set { + lvalue { + header_name: "ethernet" + field_name: "src_addr" + } + field_rvalue { + header_name: "ethernet" + field_name: "dst_addr" + } + } + } + parser_ops { + set { + lvalue { + header_name: "ethernet" + field_name: "src_addr" + } + expression_rvalue { + binary_expression { + operation: BIT_AND + left { + field_value { + header_name: "ethernet" + field_name: "src_addr" + } + } + right { hexstr_value { value: "0x00000000FFFF" } } + } + } + } + } + transition_key_fields { + field { header_name: "ethernet" field_name: "ether_type" } + } + transitions { + hex_string_transition { + value { value: "0x0800" } + mask {} + next_state: "parse_ipv4" + } + } + transitions { default_transition { next_state: $eoparser } } + optimized_symbolic_execution_info { + merge_point: $eoparser + continue_to_merge_point: true + } + } + } + parse_states { + key: "parse_ipv4" + value { + name: "parse_ipv4" + parser_ops { extract { header { header_name: "ipv4" } } } + transitions { default_transition { next_state: $eoparser } } + optimized_symbolic_execution_info { merge_point: $eoparser } + } + } + } + } + )pb", + .expected_symbolic_parsed_headers = + [](z3::context& ctx) -> absl::btree_map { + return { + {"ethernet.$valid$", ctx.bool_val(true)}, + {"ethernet.$extracted$", ctx.bool_val(true)}, + {"ethernet.dst_addr", ctx.bv_val(0xDEADBEEF, 48)}, + {"ethernet.src_addr", ctx.bv_val(0xBEEF, 48)}, + {"ipv4.$valid$", (ctx.bv_const("ethernet.ether_type", 16) == + ctx.bv_val(0x0800, 16))}, + {"ipv4.$extracted$", (ctx.bv_const("ethernet.ether_type", 16) == + ctx.bv_val(0x0800, 16))}, + }; + }, + }, + { + // Parser with primitive operations. + .ir_program_text_proto = R"pb( + parsers { + key: "parser" + value { + name: "parser" + initial_state: "start" + parse_states { + key: "start" + value { + name: "start" + parser_ops { + extract { header { header_name: "ethernet" } } + } + parser_ops { + primitive { + assignment { + left { + field_value { + header_name: "ethernet" + field_name: "$valid$" + } + } + right { bool_value { value: true } } + } + } + } + transition_key_fields { + field { header_name: "ethernet" field_name: "ether_type" } + } + transitions { + hex_string_transition { + value { value: "0x0800" } + mask {} + next_state: "parse_ipv4" + } + } + transitions { default_transition { next_state: $eoparser } } + optimized_symbolic_execution_info { + merge_point: $eoparser + continue_to_merge_point: true + } + } + } + parse_states { + key: "parse_ipv4" + value { + name: "parse_ipv4" + parser_ops { extract { header { header_name: "ipv4" } } } + parser_ops { + primitive { + assignment { + left { + field_value { + header_name: "ethernet" + field_name: "$valid$" + } + } + right { bool_value {} } + } + } + } + transitions { default_transition { next_state: $eoparser } } + optimized_symbolic_execution_info { merge_point: $eoparser } + } + } + } + } + )pb", + .expected_symbolic_parsed_headers = + [](z3::context& ctx) -> absl::btree_map { + return { + {"ethernet.$valid$", (ctx.bv_const("ethernet.ether_type", 16) != + ctx.bv_val(0x0800, 16))}, + {"ethernet.$extracted$", ctx.bool_val(true)}, + {"ipv4.$valid$", (ctx.bv_const("ethernet.ether_type", 16) == + ctx.bv_val(0x0800, 16))}, + {"ipv4.$extracted$", (ctx.bv_const("ethernet.ether_type", 16) == + ctx.bv_val(0x0800, 16))}, + }; + }, + }, + { + // Parser with masked hex string transitions. + .ir_program_text_proto = R"pb( + parsers { + key: "parser" + value { + name: "parser" + initial_state: "start" + parse_states { + key: "start" + value { + name: "start" + parser_ops { + extract { header { header_name: "ethernet" } } + } + transition_key_fields { + field { header_name: "ethernet" field_name: "ether_type" } + } + transitions { + hex_string_transition { + value { value: "0x0800" } + mask { value: "0xff00" } + next_state: "parse_ipv4" + } + } + transitions { default_transition { next_state: $eoparser } } + optimized_symbolic_execution_info { + merge_point: $eoparser + continue_to_merge_point: true + } + } + } + parse_states { + key: "parse_ipv4" + value { + name: "parse_ipv4" + parser_ops { extract { header { header_name: "ipv4" } } } + transitions { default_transition { next_state: $eoparser } } + optimized_symbolic_execution_info { merge_point: $eoparser } + } + } + } + } + )pb", + .expected_symbolic_parsed_headers = + [](z3::context& ctx) -> absl::btree_map { + return { + {"ethernet.$valid$", ctx.bool_val(true)}, + {"ethernet.$extracted$", ctx.bool_val(true)}, + {"ipv4.$valid$", + ((ctx.bv_const("ethernet.ether_type", 16) & + ctx.bv_val(0xff00, 16)) == ctx.bv_val(0x0800, 16))}, + {"ipv4.$extracted$", + ((ctx.bv_const("ethernet.ether_type", 16) & + ctx.bv_val(0xff00, 16)) == ctx.bv_val(0x0800, 16))}, + }; + }, + }, + { + // Parser with fall-through (error.NoMatch) conditions. + .ir_program_text_proto = R"pb( + parsers { + key: "parser" + value { + name: "parser" + initial_state: "start" + parse_states { + key: "start" + value { + name: "start" + parser_ops { + extract { header { header_name: "ethernet" } } + } + transition_key_fields { + field { header_name: "ethernet" field_name: "ether_type" } + } + transitions { + hex_string_transition { + value { value: "0x0800" } + mask {} + next_state: "parse_ipv4" + } + } + optimized_symbolic_execution_info { + merge_point: "parse_ipv4" + continue_to_merge_point: true + } + } + } + parse_states { + key: "parse_ipv4" + value { + name: "parse_ipv4" + parser_ops { extract { header { header_name: "ipv4" } } } + transitions { default_transition { next_state: $eoparser } } + optimized_symbolic_execution_info { + merge_point: $eoparser + continue_to_merge_point: true + } + } + } + } + } + )pb", + .expected_symbolic_parsed_headers = + [](z3::context& ctx) -> absl::btree_map { + return { + {"ethernet.$valid$", ctx.bool_val(true)}, + {"ethernet.$extracted$", ctx.bool_val(true)}, + {"ipv4.$valid$", (ctx.bv_const("ethernet.ether_type", 16) == + ctx.bv_val(0x0800, 16))}, + {"ipv4.$extracted$", (ctx.bv_const("ethernet.ether_type", 16) == + ctx.bv_val(0x0800, 16))}, + {std::string(kParserErrorField), + z3::ite((ctx.bv_const("ethernet.ether_type", 16) != + ctx.bv_val(0x0800, 16)), + ctx.bv_val(2, 32), ctx.bv_val(0, 32))}, + }; + }, + }, + }; +} + +TEST_P(EvaluateParsersTest, ValidateParsedHeadersSMTFormulae) { + // TODO: a workaround for using global Z3 context. + Z3Context(/*renew=*/true); + + // Parse the P4 program from IR text proto and evaluate the parsers. + const ParserTestParam& param = GetParam(); + ASSERT_OK_AND_ASSIGN(const ir::Dataplane data_plane, + ParseProgramTextProto(param.ir_program_text_proto)); + ASSERT_OK_AND_ASSIGN(SymbolicPerPacketState ingress_headers, + SymbolicGuardedMap::CreateSymbolicGuardedMap( + data_plane.program.headers())); + ASSERT_OK( + v1model::InitializeIngressHeaders(data_plane.program, ingress_headers)); + ASSERT_OK_AND_ASSIGN(SymbolicPerPacketState parsed_headers, + EvaluateParsers(data_plane.program, ingress_headers)); + + // Construct the expected parsed headers. + ASSERT_OK_AND_ASSIGN(SymbolicPerPacketState expected_parsed_headers, + SymbolicGuardedMap::CreateSymbolicGuardedMap( + data_plane.program.headers())); + ASSERT_OK(v1model::InitializeIngressHeaders(data_plane.program, + expected_parsed_headers)); + // Update the expected SMT formulae of certain fields specified by the test. + for (const auto& [field_name, field] : + param.expected_symbolic_parsed_headers(Z3Context())) { + ASSERT_TRUE(expected_parsed_headers.ContainsKey(field_name)); + ASSERT_OK(expected_parsed_headers.UnguardedSet(field_name, field)); + } + + std::unique_ptr solver = + std::make_unique(Z3Context()); + + // Check if the SMT formulae of the parsed headers are semantically the + // same as the expected parsed headers. + for (const auto& [field_name, actual_value] : parsed_headers) { + ASSERT_TRUE(expected_parsed_headers.ContainsKey(field_name)); + ASSERT_OK_AND_ASSIGN(z3::expr expected_value, + expected_parsed_headers.Get(field_name)); + LOG(INFO) << "Check semantic equivalence for " << field_name << ": " + << expected_value << " vs " << actual_value; + z3::expr_vector assumptions(Z3Context()); + assumptions.push_back(expected_value != actual_value); + EXPECT_EQ(solver->check(assumptions), z3::check_result::unsat); + } +} + +INSTANTIATE_TEST_SUITE_P(EvaluateParsers, EvaluateParsersTest, + ::testing::ValuesIn(GetParserTestInstances())); + } // namespace } // namespace p4_symbolic::symbolic::parser From 67d55d3c85912c2f6f960858a636e49f819a05ea Mon Sep 17 00:00:00 2001 From: bibhuprasad-hcl <161687009+bibhuprasad-hcl@users.noreply.github.com> Date: Fri, 13 Dec 2024 02:47:37 +0000 Subject: [PATCH 8/9] Adding p4info_helper_test.cc to test/lib. (#832) Co-authored-by: Srikishen Pondicherry Shanmugam --- tests/lib/BUILD.bazel | 10 +++++++++ tests/lib/p4info_helper_test.cc | 39 +++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+) create mode 100644 tests/lib/p4info_helper_test.cc diff --git a/tests/lib/BUILD.bazel b/tests/lib/BUILD.bazel index fe5d7e207..543322d0a 100644 --- a/tests/lib/BUILD.bazel +++ b/tests/lib/BUILD.bazel @@ -35,6 +35,16 @@ cc_library( ], ) +cc_test( + name = "p4info_helper_test", + srcs = ["p4info_helper_test.cc"], + deps = [ + ":p4info_helper", + "//p4_pdpi:ir_cc_proto", + "@com_google_googletest//:gtest_main", + ], +) + cc_library( name = "p4rt_fixed_table_programming_helper", testonly = True, diff --git a/tests/lib/p4info_helper_test.cc b/tests/lib/p4info_helper_test.cc new file mode 100644 index 000000000..df6a7b7f4 --- /dev/null +++ b/tests/lib/p4info_helper_test.cc @@ -0,0 +1,39 @@ +// 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 "tests/lib/p4info_helper.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" +#include "p4_pdpi/ir.pb.h" + +namespace gpins { +namespace { + +TEST(P4InfoHelperTest, TableHasMatchField) { + // Add a 'good' match field to a table definition. + pdpi::IrTableDefinition table_def; + table_def.mutable_match_fields_by_name()->insert( + {"good_match", pdpi::IrMatchFieldDefinition{}}); + + // Then add that table definition to an IrP4Info. + pdpi::IrP4Info ir_p4info; + ir_p4info.mutable_tables_by_name()->insert({"good_table", table_def}); + + EXPECT_TRUE(pins::TableHasMatchField(ir_p4info, "good_table", "good_match")); + EXPECT_FALSE(pins::TableHasMatchField(ir_p4info, "bad_table", "good_match")); + EXPECT_FALSE(pins::TableHasMatchField(ir_p4info, "good_table", "bad_match")); +} + +} // namespace +} // namespace gpins From 87c7936368a84b235e10306502e4603b7acd3c3c Mon Sep 17 00:00:00 2001 From: bibhuprasad-hcl <161687009+bibhuprasad-hcl@users.noreply.github.com> Date: Fri, 13 Dec 2024 02:48:27 +0000 Subject: [PATCH 9/9] [Thinkit] Adding L3AdmitTests. (#833) Co-authored-by: Srikishen Pondicherry Shanmugam --- tests/forwarding/l3_admit_test.cc | 467 +++++++++++++++++++++++++++++- tests/forwarding/l3_admit_test.h | 1 + 2 files changed, 463 insertions(+), 5 deletions(-) diff --git a/tests/forwarding/l3_admit_test.cc b/tests/forwarding/l3_admit_test.cc index 97d6c8a12..38fb037f9 100644 --- a/tests/forwarding/l3_admit_test.cc +++ b/tests/forwarding/l3_admit_test.cc @@ -81,18 +81,475 @@ absl::Status AddAndSetDefaultVrf(pdpi::P4RuntimeSession& session, pdpi::IrWriteRequestToPi(ir_p4info, ir_write_request)); return pdpi::SetMetadataAndSendPiWriteRequest(&session, pi_write_request); } + +absl::Status PuntAllPacketsToController(pdpi::P4RuntimeSession& session, + const pdpi::IrP4Info& ir_p4info) { + pdpi::IrWriteRequest ir_write_request; + RETURN_IF_ERROR(gutil::ReadProtoFromString( + R"pb( + updates { + type: INSERT + table_entry { + table_name: "acl_ingress_table" + priority: 2 + action { + name: "acl_trap", + params { + name: "qos_queue" + value { str: "0x1" } + } + } + } + } + )pb", + &ir_write_request)); + + ASSIGN_OR_RETURN(p4::v1::WriteRequest pi_write_request, + pdpi::IrWriteRequestToPi(ir_p4info, ir_write_request)); + return pdpi::SetMetadataAndSendPiWriteRequest(&session, pi_write_request); +} + +// L3 routing configurations that can be shared when generating the L3 routing +// flows. +struct L3Route { + std::string vrf_id; + + std::string switch_mac; + std::pair switch_ip; + + std::string peer_port; + std::string peer_mac; + std::string peer_ip; + + std::string router_interface_id; + std::string nexthop_id; +}; + +absl::Status AddL3Route(pdpi::P4RuntimeSession& session, + const pdpi::IrP4Info& ir_p4info, + const L3Route& options) { + p4::v1::WriteRequest write_request; + ASSIGN_OR_RETURN( + *write_request.add_updates(), + RouterInterfaceTableUpdate(ir_p4info, p4::v1::Update::INSERT, + options.router_interface_id, options.peer_port, + options.switch_mac)); + ASSIGN_OR_RETURN(*write_request.add_updates(), + NeighborTableUpdate(ir_p4info, p4::v1::Update::INSERT, + options.router_interface_id, + options.peer_ip, options.peer_mac)); + ASSIGN_OR_RETURN( + *write_request.add_updates(), + NexthopTableUpdate(ir_p4info, p4::v1::Update::INSERT, options.nexthop_id, + options.router_interface_id, options.peer_ip)); + ASSIGN_OR_RETURN( + *write_request.add_updates(), + Ipv4TableUpdate(ir_p4info, p4::v1::Update::INSERT, + IpTableOptions{ + .vrf_id = options.vrf_id, + .dst_addr_lpm = options.switch_ip, + .action = IpTableOptions::Action::kSetNextHopId, + .nexthop_id = options.nexthop_id, + })); + + return pdpi::SetMetadataAndSendPiWriteRequest(&session, write_request); +} + +absl::Status AdmitL3Route(pdpi::P4RuntimeSession& session, + const pdpi::IrP4Info& ir_p4info, + const L3AdmitOptions& options) { + LOG(INFO) << "Admiting L3 packets with DST MAC matching: " + << options.dst_mac.first << " & " << options.dst_mac.second; + p4::v1::WriteRequest write_request; + ASSIGN_OR_RETURN( + *write_request.add_updates(), + L3AdmitTableUpdate(ir_p4info, p4::v1::Update::INSERT, options)); + return pdpi::SetMetadataAndSendPiWriteRequest(&session, write_request); +} + +absl::StatusOr UdpPacket(absl::string_view dst_mac, + absl::string_view dst_ip, + absl::string_view payload) { + packetlib::Packet packet; + RETURN_IF_ERROR(gutil::ReadProtoFromString( + absl::Substitute( + R"pb( + headers { + ethernet_header { + ethernet_destination: "$0" + ethernet_source: "00:00:22:22:00:00" + ethertype: "0x0800" + } + } + headers { + ipv4_header { + version: "0x4" + ihl: "0x5" + dscp: "0x1b" + ecn: "0x1" + identification: "0x0000" + flags: "0x0" + fragment_offset: "0x0000" + ttl: "0x10" + protocol: "0x11" # UDP + ipv4_source: "10.0.0.1" + ipv4_destination: "$1" + } + } + headers { + udp_header { source_port: "0x0014" destination_port: "0x000a" } + } + payload: "$2" + )pb", + dst_mac, dst_ip, payload), + &packet)); + return packetlib::SerializePacket(packet); +} + +absl::Status SendUdpPacket(pdpi::P4RuntimeSession& session, + const pdpi::IrP4Info& ir_p4info, + const std::string& port_id, int packet_count, + absl::string_view dst_mac, absl::string_view dst_ip, + absl::string_view payload) { + LOG(INFO) << "Sending " << packet_count + << " packets with DST MAC: " << dst_mac; + ASSIGN_OR_RETURN(std::string packet, UdpPacket(dst_mac, dst_ip, payload)); + for (int i = 0; i < packet_count; ++i) { + RETURN_IF_ERROR(InjectEgressPacket(port_id, packet, ir_p4info, &session)); + } + return absl::OkStatus(); +} + } // namespace -TEST_P(L3AdmitTestFixture, L3PacketsAreRoutedWhenMacAddressIsInMyStation) { - LOG(INFO) << "Starting test."; +TEST_P(L3AdmitTestFixture, L3PacketsAreRoutedOnlyWhenMacAddressIsInMyStation) { + // Punt all traffic arriving at the control switch, and collect them to verify + // forwarding. + std::unique_ptr packetio_control = + std::make_unique(p4rt_control_switch_session_.get(), + PacketInHelper::NoFilter); + ASSERT_OK( + PuntAllPacketsToController(*p4rt_control_switch_session_, sai::GetIrP4Info(sai::Instantiation::kMiddleblock))); + + // Add an L3 route to enable forwarding. + L3Route l3_route{ + .vrf_id = "vrf-1", + .switch_mac = "00:00:00:00:00:01", + .switch_ip = std::make_pair("10.0.0.1", 32), + .peer_port = "1", + .peer_mac = "00:00:00:00:00:02", + .peer_ip = "10.0.0.2", + .router_interface_id = "rif-1", + .nexthop_id = "nexthop-1", + }; + ASSERT_OK(AddAndSetDefaultVrf(*p4rt_sut_switch_session_, sai::GetIrP4Info(sai::Instantiation::kMiddleblock), + l3_route.vrf_id)); + ASSERT_OK(AddL3Route(*p4rt_sut_switch_session_, sai::GetIrP4Info(sai::Instantiation::kMiddleblock), l3_route)); + + // Admit only 1 MAC address to the forwaring pipeline. + ASSERT_OK(AdmitL3Route( + *p4rt_sut_switch_session_, sai::GetIrP4Info(sai::Instantiation::kMiddleblock), + L3AdmitOptions{ + .priority = 2070, + .dst_mac = std ::make_pair("00:01:02:03:04:05", "FF:FF:FF:FF:FF:FF"), + })); + + // Send 2 sets of packets to the switch. The first set of packets should not + // match the L3 admit MAC and therefore will be dropped. The second set of + // packet should match the L3 admit MAC and therefore get forwarded. + const int kNumberOfTestPacket = 100; + + // Send the "bad" packets first to give them the most time. + const std::string kBadPayload = + "Testing L3 forwarding. This packet should be dropped."; + ASSERT_OK(SendUdpPacket(*p4rt_control_switch_session_, + sai::GetIrP4Info(sai::Instantiation::kMiddleblock), + /*port_id=*/"1", kNumberOfTestPacket, + /*dst_mac=*/"00:aa:bb:cc:cc:dd", + /*dst_ip=*/"10.0.0.1", kBadPayload)); + + // Then send the "good" packets. + const std::string kGoodPayload = + "Testing L3 forwarding. This packet should arrive to packet in."; + ASSERT_OK(SendUdpPacket(*p4rt_control_switch_session_, + sai::GetIrP4Info(sai::Instantiation::kMiddleblock), + /*port_id=*/"1", kNumberOfTestPacket, + /*dst_mac=*/"00:01:02:03:04:05", + /*dst_ip=*/"10.0.0.1", kGoodPayload)); + + absl::Time timeout = absl::Now() + absl::Minutes(1); + int good_packet_count = 0; + int bad_packet_count = 0; + while (good_packet_count < kNumberOfTestPacket) { + if (packetio_control->HasPacketInMessage()) { + ASSERT_OK_AND_ASSIGN(p4::v1::StreamMessageResponse response, + packetio_control->GetNextPacketInMessage()); + // Verify this is the packet we expect. + packetlib::Packet packet_in = + packetlib::ParsePacket(response.packet().payload()); + if (response.update_case() == p4::v1::StreamMessageResponse::kPacket && + packet_in.payload() == kGoodPayload) { + ++good_packet_count; + } else if (response.update_case() == + p4::v1::StreamMessageResponse::kPacket && + packet_in.payload() == kBadPayload) { + ++bad_packet_count; + } else { + LOG(WARNING) << "Unexpected response: " << response.DebugString(); + } + } + + if (absl::Now() > timeout) { + LOG(ERROR) << "Reached timeout waiting on packets to arrive."; + break; + } + } + LOG(INFO) << "Done collecting packets."; + + EXPECT_EQ(good_packet_count, kNumberOfTestPacket); + EXPECT_EQ(bad_packet_count, 0); +} + +TEST_P(L3AdmitTestFixture, L3AdmitCanUseMaskToAllowMultipleMacAddresses) { + // Punt all traffic arriving at the control switch, and collect them to verify + // forwarding. + std::unique_ptr packetio_control = + std::make_unique(p4rt_control_switch_session_.get(), + PacketInHelper::NoFilter); + ASSERT_OK( + PuntAllPacketsToController(*p4rt_control_switch_session_, + sai::GetIrP4Info(sai::Instantiation::kMiddleblock))); + + // Add an L3 route to enable forwarding. + L3Route l3_route{ + .vrf_id = "vrf-1", + .switch_mac = "00:00:00:00:00:01", + .switch_ip = std::make_pair("10.0.0.1", 32), + .peer_port = "1", + .peer_mac = "00:00:00:00:00:02", + .peer_ip = "10.0.0.2", + .router_interface_id = "rif-1", + .nexthop_id = "nexthop-1", + }; + + ASSERT_OK(AddAndSetDefaultVrf(*p4rt_sut_switch_session_, + sai::GetIrP4Info(sai::Instantiation::kMiddleblock), + l3_route.vrf_id)); + ASSERT_OK(AddL3Route(*p4rt_sut_switch_session_, + sai::GetIrP4Info(sai::Instantiation::kMiddleblock), + l3_route)); + + // Admit multiple MAC addresses into L3 routing with a single L3 admit rule. + ASSERT_OK(AdmitL3Route( + *p4rt_sut_switch_session_, sai::GetIrP4Info(sai::Instantiation::kMiddleblock), + L3AdmitOptions{ + .priority = 2070, + .dst_mac = std ::make_pair("00:01:02:03:00:05", "FF:FF:FF:FF:F0:FF"), + })); + + // Send 5 sets of packets to the switch each with a different MAC address that + // matches the L3Admit rule's mask. + const int kNumberOfTestPacket = 20; + const std::string kGoodPayload = + "Testing L3 forwarding. This packet should arrive to packet in."; + for (int i = 0; i < 5; ++i) { + std::string dst_mac = absl::StrFormat("00:01:02:03:%02d:05", i); + ASSERT_OK(SendUdpPacket(*p4rt_control_switch_session_, + sai::GetIrP4Info(sai::Instantiation::kMiddleblock), + /*port_id=*/"1", kNumberOfTestPacket, dst_mac, + /*dst_ip=*/"10.0.0.1", kGoodPayload)); + } + + absl::Time timeout = absl::Now() + absl::Minutes(1); + int good_packet_count = 0; + while (good_packet_count < 5 * kNumberOfTestPacket) { + if (packetio_control->HasPacketInMessage()) { + ASSERT_OK_AND_ASSIGN(p4::v1::StreamMessageResponse response, + packetio_control->GetNextPacketInMessage()); + + // Verify this is the packet we expect. + packetlib::Packet packet_in = + packetlib::ParsePacket(response.packet().payload()); + if (response.update_case() == p4::v1::StreamMessageResponse::kPacket && + packet_in.payload() == kGoodPayload) { + ++good_packet_count; + } else { + LOG(WARNING) << "Unexpected response: " << response.DebugString(); + } + } + + if (absl::Now() > timeout) { + LOG(ERROR) << "Reached timeout waiting on packets to arrive."; + break; + } + } - // PacketIO handlers for both the SUT and control switch. - std::unique_ptr packetio_sut = - std::make_unique(p4rt_sut_switch_session_.get(), + LOG(INFO) << "Done collecting packets."; + + EXPECT_EQ(good_packet_count, 5 * kNumberOfTestPacket); +} + +TEST_P(L3AdmitTestFixture, L3AdmitCanUseInPortToRestrictMacAddresses) { + // Punt all traffic arriving at the control switch, and collect them to verify + // forwarding. + std::unique_ptr packetio_control = + std::make_unique(p4rt_control_switch_session_.get(), PacketInHelper::NoFilter); + ASSERT_OK( + PuntAllPacketsToController(*p4rt_control_switch_session_, + sai::GetIrP4Info(sai::Instantiation::kMiddleblock))); + + // Add an L3 route to enable forwarding. + L3Route l3_route{ + .vrf_id = "vrf-1", + .switch_mac = "00:00:00:00:00:01", + .switch_ip = std::make_pair("10.0.0.1", 32), + .peer_port = "1", + .peer_mac = "00:00:00:00:00:02", + .peer_ip = "10.0.0.2", + .router_interface_id = "rif-1", + .nexthop_id = "nexthop-1", + }; + ASSERT_OK(AddAndSetDefaultVrf(*p4rt_sut_switch_session_, + sai::GetIrP4Info(sai::Instantiation::kMiddleblock), + l3_route.vrf_id)); + ASSERT_OK(AddL3Route(*p4rt_sut_switch_session_, + sai::GetIrP4Info(sai::Instantiation::kMiddleblock), + l3_route)); + + // Admit the MAC addresses only on port XYZ + ASSERT_OK(AdmitL3Route( + *p4rt_sut_switch_session_, + sai::GetIrP4Info(sai::Instantiation::kMiddleblock), + L3AdmitOptions{ + .priority = 2070, + .dst_mac = std ::make_pair("00:01:02:03:00:05", "FF:FF:FF:FF:F0:FF"), + .in_port = "2", + })); + + // Send 2 sets of packets to the switch. The first set of packets should not + // match the L3 admit port and therefore will be dropped. The second set of + // packet should match the L3 admit port and therefore get forwarded. + const int kNumberOfTestPacket = 100; + + // Send the "bad" packets first to give them the most time. + const std::string kBadPayload = + "Testing L3 forwarding. This packet should be dropped."; + ASSERT_OK(SendUdpPacket(*p4rt_control_switch_session_, + sai::GetIrP4Info(sai::Instantiation::kMiddleblock), + /*port_id=*/"1", kNumberOfTestPacket, + /*dst_mac=*/"00:01:02:03:04:05", + /*dst_ip=*/"10.0.0.1", kBadPayload)); + + // Then send the "good" packets. + const std::string kGoodPayload = + "Testing L3 forwarding. This packet should arrive to packet in."; + ASSERT_OK(SendUdpPacket(*p4rt_control_switch_session_, + sai::GetIrP4Info(sai::Instantiation::kMiddleblock), + /*port_id=*/"2", kNumberOfTestPacket, + /*dst_mac=*/"00:01:02:03:04:05", + /*dst_ip=*/"10.0.0.1", kGoodPayload)); + + absl::Time timeout = absl::Now() + absl::Minutes(1); + int good_packet_count = 0; + int bad_packet_count = 0; + while (good_packet_count < kNumberOfTestPacket) { + if (packetio_control->HasPacketInMessage()) { + ASSERT_OK_AND_ASSIGN(p4::v1::StreamMessageResponse response, + packetio_control->GetNextPacketInMessage()); + + // Verify this is the packet we expect. + packetlib::Packet packet_in = + packetlib::ParsePacket(response.packet().payload()); + if (response.update_case() == p4::v1::StreamMessageResponse::kPacket && + packet_in.payload() == kGoodPayload) { + ++good_packet_count; + } else if (response.update_case() == + p4::v1::StreamMessageResponse::kPacket && + packet_in.payload() == kBadPayload) { + ++bad_packet_count; + } else { + LOG(WARNING) << "Unexpected response: " << response.DebugString(); + } + } + + if (absl::Now() > timeout) { + LOG(ERROR) << "Reached timeout waiting on packets to arrive."; + break; + } + } + LOG(INFO) << "Done collecting packets."; + + EXPECT_EQ(good_packet_count, kNumberOfTestPacket); + EXPECT_EQ(bad_packet_count, 0); +} + +TEST_P(L3AdmitTestFixture, L3PacketsCanBeRoutedWithOnlyARouterInterface) { + // Punt all traffic arriving at the control switch, and collect them to verify + // forwarding. std::unique_ptr packetio_control = std::make_unique(p4rt_control_switch_session_.get(), PacketInHelper::NoFilter); +ASSERT_OK( + PuntAllPacketsToController(*p4rt_control_switch_session_, + sai::GetIrP4Info(sai::Instantiation::kMiddleblock))); + +// Add an L3 route to enable forwarding, but do not add an explicit L3Admit + // rule. + L3Route l3_route{ + .vrf_id = "vrf-1", + .switch_mac = "00:00:00:00:00:01", + .switch_ip = std::make_pair("10.0.0.1", 32), + .peer_port = "1", + .peer_mac = "00:00:00:00:00:02", + .peer_ip = "10.0.0.2", + .router_interface_id = "rif-1", + .nexthop_id = "nexthop-1", + }; + ASSERT_OK(AddAndSetDefaultVrf(*p4rt_sut_switch_session_, + sai::GetIrP4Info(sai::Instantiation::kMiddleblock), + l3_route.vrf_id)); + ASSERT_OK(AddL3Route(*p4rt_sut_switch_session_, + sai::GetIrP4Info(sai::Instantiation::kMiddleblock), + l3_route)); + + // Send 1 set of packets to the switch using the switch's MAC address from the + // L3 route. + const int kNumberOfTestPacket = 100; + const std::string kGoodPayload = + "Testing L3 forwarding. This packet should arrive to packet in."; + ASSERT_OK(SendUdpPacket(*p4rt_control_switch_session_, + sai::GetIrP4Info(sai::Instantiation::kMiddleblock), + /*port_id=*/"1", kNumberOfTestPacket, + /*dst_mac=*/"00:00:00:00:00:01", + /*dst_ip=*/"10.0.0.1", kGoodPayload)); + + absl::Time timeout = absl::Now() + absl::Minutes(1); + int good_packet_count = 0; + while (good_packet_count < kNumberOfTestPacket) { + if (packetio_control->HasPacketInMessage()) { + ASSERT_OK_AND_ASSIGN(p4::v1::StreamMessageResponse response, + packetio_control->GetNextPacketInMessage()); + + // Verify this is the packet we expect. + packetlib::Packet packet_in = + packetlib::ParsePacket(response.packet().payload()); + if (response.update_case() == p4::v1::StreamMessageResponse::kPacket && + packet_in.payload() == kGoodPayload) { + ++good_packet_count; + } else { + LOG(WARNING) << "Unexpected response: " << response.DebugString(); + } + } + + if (absl::Now() > timeout) { + LOG(ERROR) << "Reached timeout waiting on packets to arrive."; + break; + } + } + LOG(INFO) << "Done collecting packets."; + + EXPECT_EQ(good_packet_count, kNumberOfTestPacket); } } // namespace pins diff --git a/tests/forwarding/l3_admit_test.h b/tests/forwarding/l3_admit_test.h index b64889ff5..690bf489c 100644 --- a/tests/forwarding/l3_admit_test.h +++ b/tests/forwarding/l3_admit_test.h @@ -28,6 +28,7 @@ namespace pins { class L3AdmitTestFixture : public thinkit::MirrorTestbedFixture { protected: + void SetUp() override; // This test runs on a mirror testbed setup so we open a P4RT connection to // both switches.