diff --git a/tests/lib/BUILD.bazel b/tests/lib/BUILD.bazel index 130c8c4f..fe5d7e20 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.cc b/tests/lib/packet_generator.cc new file mode 100644 index 00000000..ba5e18e1 --- /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 diff --git a/tests/lib/packet_generator.h b/tests/lib/packet_generator.h new file mode 100644 index 00000000..bc1e72da --- /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_ diff --git a/tests/lib/packet_generator_test.cc b/tests/lib/packet_generator_test.cc new file mode 100644 index 00000000..df536518 --- /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 diff --git a/tests/sflow/BUILD.bazel b/tests/sflow/BUILD.bazel index befd8ac5..4c7d88de 100644 --- a/tests/sflow/BUILD.bazel +++ b/tests/sflow/BUILD.bazel @@ -20,10 +20,34 @@ package( cc_library( name = "sflow_test", testonly = True, + srcs = ["sflow_test.cc"], hdrs = ["sflow_test.h"], deps = [ - "//thinkit:mirror_testbed_fixture", + "//gutil:collections", + "//gutil:status_matchers", + "//gutil:testing", + "//lib:ixia_helper", + "//lib/gnmi:gnmi_helper", + "//lib/validator:validator_lib", + "//p4_pdpi:ir", + "//p4_pdpi:p4_runtime_session", + "//p4_pdpi:pd", + "//p4_pdpi/packetlib", + "//p4_pdpi/string_encodings:decimal_string", + "//sai_p4/instantiations/google:sai_pd_cc_proto", + "//tests/lib:switch_test_setup_helpers", + "//thinkit:generic_testbed", + "//thinkit:generic_testbed_fixture", + "//thinkit:ssh_client", + "@com_github_gnmi//proto/gnmi:gnmi_cc_grpc_proto", "@com_github_grpc_grpc//:grpc++", + "@com_google_absl//absl/memory", + "@com_google_absl//absl/status", + "@com_google_absl//absl/status:statusor", + "@com_google_absl//absl/strings", + "@com_google_absl//absl/synchronization", + "@com_google_absl//absl/time", + "@com_google_googletest//:gtest", ], alwayslink = True, ) diff --git a/tests/sflow/sflow_test.cc b/tests/sflow/sflow_test.cc new file mode 100644 index 00000000..736184e8 --- /dev/null +++ b/tests/sflow/sflow_test.cc @@ -0,0 +1,587 @@ +// 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/sflow/sflow_test.h" + +#include +#include +#include +#include // NOLINT +#include +#include + +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "absl/strings/str_split.h" +#include "absl/strings/string_view.h" +#include "absl/strings/substitute.h" +#include "absl/synchronization/mutex.h" +#include "absl/time/clock.h" +#include "absl/time/time.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" +#include "gutil/collections.h" +#include "gutil/status.h" +#include "gutil/status_matchers.h" +#include "gutil/testing.h" +#include "lib/gnmi/gnmi_helper.h" +#include "lib/ixia_helper.h" +#include "lib/validator/validator_lib.h" +#include "p4_pdpi/packetlib/packetlib.h" +#include "p4_pdpi/pd.h" +#include "p4_pdpi/string_encodings/decimal_string.h" +#include "sai_p4/instantiations/google/sai_pd.pb.h" +#include "tests/lib/switch_test_setup_helpers.h" +#include "thinkit/generic_testbed.h" +#include "thinkit/ssh_client.h" + +namespace pins { + +namespace { +// Number of packets sent to one port. +constexpr int kPacketsNum = 4000000; + +// Number of packets sent per second. +constexpr int kPacketsPerSecond = 160000; + +// The maximum number of bytes that should be copied from a sampled packet to +// the sFlow datagram. +constexpr int kSampleSize = 128; + +// Once accumulated data reaches kMaxPacketSize, sFlow would generate an sFlow +// datagram. +constexpr int kMaxPacketSize = 1400; + +// Sflowtool binary name in the collector. +constexpr absl::string_view kSflowToolName = "sflowtool"; +constexpr absl::string_view kSflowCommandTemplate = + "/etc/init.d/sflow-container exec '$0 -l -p 6343 &" + " pid=$$!; sleep $1; kill -9 $$pid;'"; + +// IpV4 address for filtering the sflow packet. +constexpr uint32_t kIpV4Src = 0x01020304; // 1.2.3.4 +// Ixia flow details. +constexpr auto kDstMac = netaddr::MacAddress(02, 02, 02, 02, 02, 02); +constexpr auto kSourceMac = netaddr::MacAddress(00, 01, 02, 03, 04, 05); +constexpr auto kIpV4Dst = netaddr::Ipv4Address(192, 168, 10, 1); + +// TODO: Parse the sampling rate from config or state path. +constexpr int kSamplingRateInterval = 4000; + +// Buffering and software bottlenecks can cause some amount of variance in rate +// measured end to end. +// Level of tolerance for packet rate verification. +// This could be parameterized in future if this is platform dependent. +constexpr double kTolerance = 0.1; + +constexpr absl::string_view kSpeed100GB = + "\"openconfig-if-ethernet:SPEED_100GB\""; +constexpr absl::string_view kSpeed200GB = + "\"openconfig-if-ethernet:SPEED_200GB\""; + +std::string GetSrcIpv4AddrByPortId(const int port_id) { + return netaddr::Ipv4Address(std::bitset<32>(kIpV4Src + port_id)).ToString(); +} + +// These are the counters we track in these tests. +struct Counters { + uint64_t in_pkts; + uint64_t out_pkts; + uint64_t in_octets; + uint64_t out_octets; +}; + +absl::StatusOr GetGnmiStat(std::string stat_name, + absl::string_view iface, + gnmi::gNMI::StubInterface* gnmi_stub) { + std::string ops_state_path; + std::string ops_parse_str; + + if (absl::StartsWith(stat_name, "ipv4-")) { + std::string name = stat_name.substr(5); + ops_state_path = absl::StrCat( + "interfaces/interface[name=", iface, + "]subinterfaces/subinterface[index=0]/ipv4/state/counters/", name); + ops_parse_str = "openconfig-if-ip:" + name; + } else if (absl::StartsWith(stat_name, "ipv6-")) { + std::string name = stat_name.substr(5); + ops_state_path = absl::StrCat( + "interfaces/interface[name=", iface, + "]subinterfaces/subinterface[index=0]/ipv6/state/counters/", name); + ops_parse_str = "openconfig-if-ip:" + name; + } else { + ops_state_path = absl::StrCat("interfaces/interface[name=", iface, + "]/state/counters/", stat_name); + ops_parse_str = "openconfig-interfaces:" + stat_name; + } + + ASSIGN_OR_RETURN(std::string ops_response, + pins_test::GetGnmiStatePathInfo(gnmi_stub, ops_state_path, + ops_parse_str)); + + uint64_t stat; + // skip over the initial quote '"' + (void)absl::SimpleAtoi(ops_response.substr(1), &stat); + return stat; +} + +void ShowCounters(const Counters& cnt) { + LOG(INFO) << "in-pkts " << cnt.in_pkts; + LOG(INFO) << "out-pkts " << cnt.out_pkts; + LOG(INFO) << "in-octets " << cnt.in_octets; + LOG(INFO) << "out-octets " << cnt.out_octets; +} + +// DeltaCounters - computer delta as change from initial to final counters +Counters DeltaCounters(const Counters& initial, const Counters& final) { + Counters delta = {}; + + delta.in_pkts = final.in_pkts - initial.in_pkts; + delta.out_pkts = final.out_pkts - initial.out_pkts; + delta.in_octets = final.in_octets - initial.in_octets; + delta.out_octets = final.out_octets - initial.out_octets; + return delta; +} + +absl::StatusOr ReadCounters(std::string iface, + gnmi::gNMI::StubInterface* gnmi_stub) { + Counters cnt = {}; + + ASSIGN_OR_RETURN(cnt.in_pkts, GetGnmiStat("in-pkts", iface, gnmi_stub)); + ASSIGN_OR_RETURN(cnt.out_pkts, GetGnmiStat("out-pkts", iface, gnmi_stub)); + ASSIGN_OR_RETURN(cnt.in_octets, GetGnmiStat("in-octets", iface, gnmi_stub)); + ASSIGN_OR_RETURN(cnt.out_octets, GetGnmiStat("out-octets", iface, gnmi_stub)); + return cnt; +} + +// The packets are all same for one port. Use port_id as the index for +// generating packets. +absl::Status SendNPacketsToSut(absl::Span traffic_ref, + absl::string_view topology_ref, + absl::Duration runtime, + thinkit::GenericTestbed& testbed) { + // Send Ixia traffic. + RETURN_IF_ERROR( + pins_test::ixia::StartTraffic(traffic_ref, topology_ref, testbed)); + + // Wait for Traffic to be sent. + absl::SleepFor(runtime); + + // Stop Ixia traffic. + RETURN_IF_ERROR(pins_test::ixia::StopTraffic(traffic_ref, testbed)); + + return absl::OkStatus(); +} + +// Set up Ixia traffic with given parameters and return the traffic ref and +// topology ref string. +absl::StatusOr, std::string>> +SetUpIxiaTraffic(absl::Span ixia_links, + thinkit::GenericTestbed& testbed, const int pkt_count, + const int pkt_rate, const int frame_size = 1000) { + std::vector traffic_refs; + std::string topology_ref; + for (const IxiaLink& ixia_link : ixia_links) { + LOG(INFO) << __func__ << " Ixia if:" << ixia_link.ixia_interface + << " sut if:" << ixia_link.sut_interface + << " port id:" << ixia_link.port_id; + + std::string ixia_interface = ixia_link.ixia_interface; + std::string sut_interface = ixia_link.sut_interface; + + // Set up Ixia traffic. + ASSIGN_OR_RETURN(pins_test::ixia::IxiaPortInfo ixia_port, + pins_test::ixia::ExtractPortInfo(ixia_interface)); + ASSIGN_OR_RETURN(std::string topology_ref_tmp, + pins_test::ixia::IxiaConnect(ixia_port.hostname, testbed)); + if (topology_ref.empty()) { + topology_ref = topology_ref_tmp; + } else { + EXPECT_EQ(topology_ref, topology_ref_tmp); + } + + ASSIGN_OR_RETURN(std::string vport_ref, + pins_test::ixia::IxiaVport(topology_ref, ixia_port.card, + ixia_port.port, testbed)); + + ASSIGN_OR_RETURN(std::string traffic_ref, + pins_test::ixia::IxiaSession(vport_ref, testbed)); + + RETURN_IF_ERROR( + pins_test::ixia::SetFrameRate(traffic_ref, pkt_rate, testbed)); + + RETURN_IF_ERROR( + pins_test::ixia::SetFrameCount(traffic_ref, pkt_count, testbed)); + + RETURN_IF_ERROR( + pins_test::ixia::SetFrameSize(traffic_ref, frame_size, testbed)); + + RETURN_IF_ERROR(pins_test::ixia::SetSrcMac(traffic_ref, + kSourceMac.ToString(), testbed)); + + RETURN_IF_ERROR( + pins_test::ixia::SetDestMac(traffic_ref, kDstMac.ToString(), testbed)); + + RETURN_IF_ERROR(pins_test::ixia::AppendIPv4(traffic_ref, testbed)); + + // Use Ipv4 source address to differentiate different ports. + RETURN_IF_ERROR(pins_test::ixia::SetSrcIPv4( + traffic_ref, GetSrcIpv4AddrByPortId(ixia_link.port_id), testbed)); + + RETURN_IF_ERROR(pins_test::ixia::SetDestIPv4(traffic_ref, + kIpV4Dst.ToString(), testbed)); + traffic_refs.push_back(traffic_ref); + } + return std::make_pair(traffic_refs, topology_ref); +} + +// Get the packet counters on SUT interface connected to Ixia. +absl::StatusOr> GetIxiaInterfaceCounters( + absl::Span ixia_links, + gnmi::gNMI::StubInterface* gnmi_stub) { + std::vector counters; + for (const IxiaLink& ixia_link : ixia_links) { + ASSIGN_OR_RETURN(auto initial_in_counter, + ReadCounters(ixia_link.sut_interface, gnmi_stub)); + LOG(INFO) << "Ingress Counters (" << ixia_link.sut_interface << "):\n"; + ShowCounters(initial_in_counter); + LOG(INFO) << "\n"; + counters.push_back(initial_in_counter); + } + // Reads CPU counter. + ASSIGN_OR_RETURN(auto initial_in_counter, ReadCounters("CPU", gnmi_stub)); + LOG(INFO) << "Ingress Counters (\"CPU\"):\n"; + ShowCounters(initial_in_counter); + LOG(INFO) << "\n"; + counters.push_back(initial_in_counter); + return counters; +} + +// Run sflowtool on SUT in a new thread. Returns the thread to let caller to +// wait for the finish. +absl::StatusOr StartSflowCollector( + thinkit::SSHClient* ssh_client, absl::string_view device_name, + const int sflowtool_runtime, std::string& sflow_tool_result) { + std::thread sflow_tool_thread = std::thread( + [&sflow_tool_result, ssh_client, device_name, sflowtool_runtime]() { + const std::string ssh_command = absl::Substitute( + kSflowCommandTemplate, kSflowToolName, sflowtool_runtime); + LOG(INFO) << "ssh command:" << ssh_command; + ASSERT_OK_AND_ASSIGN( + sflow_tool_result, + ssh_client->RunCommand( + device_name, ssh_command, + /*timeout=*/absl::Seconds(sflowtool_runtime + 2))); + }); + // Sleep to wait sflowtool to start. + absl::SleepFor(absl::Seconds(5)); + return sflow_tool_thread; +} + +// Send packets to SUT and validate packet counters via gNMI. +absl::Status SendSflowTraffic(const std::vector& traffic_refs, + const std::string& topology_ref, + absl::Span ixia_links, + thinkit::GenericTestbed& testbed, + gnmi::gNMI::StubInterface* gnmi_stub, + const int pkt_count, const int pkt_rate) { + // Send packets to SUT. + const absl::Time start_time = absl::Now(); + + // Read initial counters via GNMI from the SUT + LOG(INFO) << "Read initial packet counters."; + ASSIGN_OR_RETURN(std::vector initial_in_counters, + GetIxiaInterfaceCounters(ixia_links, gnmi_stub)); + + RETURN_IF_ERROR(SendNPacketsToSut( + traffic_refs, topology_ref, + /*runtime=*/absl::Seconds(std::ceil(1.0f * kPacketsNum / pkt_rate)), + testbed)); + + LOG(INFO) << "Sent " << kPacketsNum << " packets in " + << (absl::Now() - start_time) << "\n"; + + LOG(INFO) << "Read final packet counters."; + // Read final counters via GNMI from the SUT + ASSIGN_OR_RETURN(std::vector final_in_counters, + GetIxiaInterfaceCounters(ixia_links, gnmi_stub)); + for (size_t i = 0; i < ixia_links.size(); ++i) { + auto delta = DeltaCounters(initial_in_counters[i], final_in_counters[i]); + // Display the difference in the counters for now (during test dev) + LOG(INFO) << "\nIngress Deltas (" << ixia_links[i].sut_interface << "):\n"; + ShowCounters(delta); + EXPECT_EQ(delta.in_pkts, pkt_count) + << "Received packets count is not equal to sent packets count: " + << ". Port id: " << ixia_links[i].port_id << ". Sent " << pkt_count + << ". Received " << delta.in_pkts << "."; + } + // Show CPU counter data. + auto delta = + DeltaCounters(initial_in_counters.back(), final_in_counters.back()); + LOG(INFO) << "\nIngress Deltas (\"CPU\"):\n"; + ShowCounters(delta); + return absl::OkStatus(); +} + +int GetSflowSamplesOnSut(const std::string& sflowtool_output, + const int port_id) { + constexpr int kFieldSize = 20, kSrcIpIdx = 9; + int count = 0; + // Each line indicates one sFlow sample. + for (absl::string_view sflow : absl::StrSplit(sflowtool_output, '\n')) { + // Split by column. + std::vector fields = absl::StrSplit(sflow, ','); + if (fields.size() < kFieldSize) { + continue; + } + // Filter source ip. + if (fields[kSrcIpIdx] == GetSrcIpv4AddrByPortId(port_id)) { + count++; + } + } + return count; +} + +// Get port speed by reading interface/ethernet/state/port-speed path. +absl::StatusOr GetPortSpeed(absl::string_view iface, + gnmi::gNMI::StubInterface* gnmi_stub) { + std::string ops_state_path = absl::StrCat("interfaces/interface[name=", iface, + "]/ethernet/state/port-speed"); + + std::string ops_parse_str = "openconfig-if-ethernet:port-speed"; + return pins_test::GetGnmiStatePathInfo(gnmi_stub, ops_state_path, + ops_parse_str); +} + +// Check interface/state/oper-status value to validate if link is up. +absl::StatusOr CheckLinkUp(absl::string_view interface, + gnmi::gNMI::StubInterface& gnmi_stub) { + std::string oper_status_state_path = absl::StrCat( + "interfaces/interface[name=", interface, "]/state/oper-status"); + + std::string parse_str = "openconfig-interfaces:oper-status"; + ASSIGN_OR_RETURN(std::string ops_response, + pins_test::GetGnmiStatePathInfo( + &gnmi_stub, oper_status_state_path, parse_str)); + + return ops_response == "\"UP\""; +} + +// Returns a vector of SUT interfaces that are connected to Ixia and up. +absl::StatusOr> GetIxiaConnectedUpLinks( + thinkit::GenericTestbed& generic_testbed, + gnmi::gNMI::StubInterface& gnmi_stub) { + std::vector ixia_links; + + absl::flat_hash_map interface_info = + generic_testbed.GetSutInterfaceInfo(); + absl::flat_hash_map port_id_per_port_name; + ASSIGN_OR_RETURN(port_id_per_port_name, + pins_test::GetAllInterfaceNameToPortId(gnmi_stub)); + // Loop through the interface_info looking for Ixia/SUT interface pairs, + // checking if the link is up. Add the pair to connections. + for (const auto& [interface, info] : interface_info) { + if (info.interface_modes.contains(thinkit::TRAFFIC_GENERATOR)) { + ASSIGN_OR_RETURN(bool sut_link_up, CheckLinkUp(interface, gnmi_stub)); + auto port_id = gutil::FindOrNull(port_id_per_port_name, interface); + EXPECT_NE(port_id, nullptr) << absl::Substitute( + "No corresponding p4rt id for interface $0", interface); + if (sut_link_up) { + LOG(INFO) << "Ixia interface:" << info.peer_interface_name + << ". Sut interface:" << interface << ". Port id:" + << *port_id; + ixia_links.push_back(IxiaLink{ + .ixia_interface = info.peer_interface_name, + .sut_interface = interface, + .port_id = std::stoi(*port_id), + }); + } + } + } + + return ixia_links; +} + +// Used for printing result. +struct SflowResult { + std::string sut_interface; + int packets; + int sampling_rate; + int expected_samples; + int actual_samples; + + std::string DebugString() { + return absl::Substitute( + "Ingress interface: $0\n" + "Total packets input: $1\n" + "Sampling rate: 1 in $2\n" + "Expected samples: $3\n" + "Actual samples: $4", + sut_interface, packets, sampling_rate, expected_samples, + actual_samples); + } +}; + +} // namespace + +void SflowTestFixture::SetUp() { + // Pick a testbed with an Ixia Traffic Generator. + auto requirements = + gutil::ParseProtoOrDie( + R"pb(interface_requirements { + count: 1 + interface_modes: TRAFFIC_GENERATOR + })pb"); + + ASSERT_OK_AND_ASSIGN( + testbed_, + GetParam().testbed_interface->GetTestbedWithRequirements(requirements)); + + const std::string gnmi_config = GetParam().gnmi_config; + ASSERT_OK(testbed_->Environment().StoreTestArtifact("gnmi_config.txt", + gnmi_config)); + ASSERT_OK(testbed_->Environment().StoreTestArtifact( + "p4info.pb.txt", GetP4Info().DebugString())); + ASSERT_OK_AND_ASSIGN(sut_p4_session_, + pins_test::ConfigureSwitchAndReturnP4RuntimeSession( + testbed_->Sut(), gnmi_config, GetP4Info())); + ASSERT_OK_AND_ASSIGN(ir_p4_info_, pdpi::CreateIrP4Info(GetP4Info())); + + ASSERT_OK_AND_ASSIGN(gnmi_stub_, testbed_->Sut().CreateGnmiStub()); + // TODO: Remove unused set speed in sflow test. + // Go through all the ports that connect to the Ixia and set them + // first to 200GB. + absl::flat_hash_map interface_info = + testbed_->GetSutInterfaceInfo(); + for (const auto& [interface, info] : interface_info) { + if (info.interface_modes.contains(thinkit::TRAFFIC_GENERATOR)) { + ASSERT_OK(pins_test::SetPortSpeedInBitsPerSecond(std::string(kSpeed200GB), + interface, *gnmi_stub_)); + } + } + + auto speed_config_applied = + [&interface_info](absl::string_view expected_speed, + gnmi::gNMI::StubInterface* gnmi_stub) -> absl::Status { + for (const auto& [interface, info] : interface_info) { + if (info.interface_modes.contains(thinkit::TRAFFIC_GENERATOR)) { + ASSIGN_OR_RETURN(auto port_speed, GetPortSpeed(interface, gnmi_stub)); + if (port_speed != expected_speed) { + return absl::FailedPreconditionError(absl::Substitute( + "Port speed is not converged. Interface $0 " + "speed state path value is $1, expected speed is $2.", + interface, port_speed, expected_speed)); + } + } + } + return absl::OkStatus(); + }; + // Waits for speed config to be applied. + EXPECT_OK(pins_test::WaitForCondition(speed_config_applied, absl::Seconds(30), + kSpeed200GB, gnmi_stub_.get())); + auto links_up = [this]() -> absl::Status { + ASSIGN_OR_RETURN(ready_links_, + GetIxiaConnectedUpLinks(*testbed_, *gnmi_stub_)); + if (ready_links_.empty()) { + return absl::FailedPreconditionError("No Ixia links up."); + } + return absl::OkStatus(); + }; + // Waits for links to be up. + EXPECT_OK(pins_test::WaitForCondition(links_up, absl::Seconds(30))); + + // If links didn't come, lets try 100GB as some testbeds have 100GB + // IXIA connections. + if (ready_links_.empty()) { + for (const auto& [interface, info] : interface_info) { + if (info.interface_modes.contains(thinkit::TRAFFIC_GENERATOR)) { + ASSERT_OK(pins_test::SetPortSpeedInBitsPerSecond( + std::string(kSpeed100GB), interface, *gnmi_stub_)); + } + } + // Waits for speed config to be applied. + EXPECT_OK(pins_test::WaitForCondition(speed_config_applied, + absl::Seconds(30), kSpeed100GB, + gnmi_stub_.get())); + // Waits for links to come up. + EXPECT_OK(pins_test::WaitForCondition(links_up, absl::Seconds(30))); + } + ASSERT_FALSE(ready_links_.empty()) << "Ixia links are not ready"; +} + +void SflowTestFixture::TearDown() { + // Clear table entries and stop RPC sessions. + LOG(INFO) << "\n------ TearDown START ------\n"; + if (sut_p4_session_ != nullptr) { + EXPECT_OK(sut_p4_session_->Finish()); + } + GetParam().testbed_interface->TearDown(); + if (ssh_client_ != nullptr) { + delete ssh_client_; + ssh_client_ = nullptr; + } + if (GetParam().testbed_interface != nullptr) { + delete GetParam().testbed_interface; + } + LOG(INFO) << "\n------ TearDown END ------\n"; +} + +// This test checks sFlow works as expected with no rules. +// 1. Set up Ixia traffic and send packets to SUT via Ixia. +// 2. Collect sFlow samples via sflowtool on SUT. +// 3. Validate the result is as expected. +TEST_P(SflowTestFixture, CheckIngressSflowSamplePackets) { + // ixia_ref_pair would include the traffic reference and topology reference + // which could be used to send traffic later. + std::pair, std::string> ixia_ref_pair; + // Set up Ixia traffic. + ASSERT_OK_AND_ASSIGN( + ixia_ref_pair, SetUpIxiaTraffic({ready_links_[0]}, *testbed_, kPacketsNum, + kPacketsPerSecond)); + + // Start sflowtool on SUT. + std::string sflow_result; + ASSERT_OK_AND_ASSIGN( + std::thread sflow_tool_thread, + StartSflowCollector( + ssh_client_, testbed_->Sut().ChassisName(), + /*sflowtool_runtime=*/kPacketsNum / kPacketsPerSecond + 30, + sflow_result)); + // Send packets from Ixia to SUT. + ASSERT_OK(SendSflowTraffic(ixia_ref_pair.first, ixia_ref_pair.second, + {ready_links_[0]}, *testbed_, gnmi_stub_.get(), + kPacketsNum, kPacketsPerSecond)); + // Wait for sflowtool to finish. + if (sflow_tool_thread.joinable()) { + sflow_tool_thread.join(); + } + LOG(INFO) << "SFLOW result is:" << sflow_result; + // Verify sflowtool result. Since we use port id to generate packets, we use + // port id to filter sflow packets. + const int sflow_count = + GetSflowSamplesOnSut(sflow_result, ready_links_[0].port_id); + const double expected_count = 1.0 * kPacketsNum / kSamplingRateInterval; + SflowResult result = SflowResult{ + .sut_interface = ready_links_[0].sut_interface, + .packets = kPacketsNum, + .sampling_rate = kSamplingRateInterval, + .expected_samples = static_cast(expected_count), + .actual_samples = sflow_count, + }; + LOG(INFO) << "------ Test result ------\n" << result.DebugString(); + EXPECT_GE(sflow_count, expected_count * (1 - kTolerance)); + EXPECT_LE(sflow_count, expected_count * (1 + kTolerance)); +} + +} // namespace pins diff --git a/tests/sflow/sflow_test.h b/tests/sflow/sflow_test.h index fd461cd3..efcf7f50 100644 --- a/tests/sflow/sflow_test.h +++ b/tests/sflow/sflow_test.h @@ -1,13 +1,67 @@ +// 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_SFLOW_SFLOW_TEST_H_ #define PINS_TESTS_SFLOW_SFLOW_TEST_H_ -#include "thinkit/mirror_testbed_fixture.h" +#include +#include +#include // NOLINT: Need threads (instead of fiber) for upstream code. +#include + +#include "absl/memory/memory.h" +#include "gtest/gtest.h" +#include "p4_pdpi/ir.h" +#include "p4_pdpi/p4_runtime_session.h" +#include "proto/gnmi/gnmi.grpc.pb.h" +#include "thinkit/generic_testbed_fixture.h" +#include "thinkit/ssh_client.h" namespace pins { -// TODO: to be implemented -class SflowTestFixture : public thinkit::MirrorTestbedFixture { +struct SflowTestParams { + thinkit::GenericTestbedInterface* testbed_interface; + thinkit::SSHClient* ssh_client; + std::string gnmi_config; + p4::config::v1::P4Info p4_info; +}; + +// Structure represents a link between SUT and Ixia. +// This is represented by Ixia interface name and the SUT's gNMI interface +// name and its corrosponding p4 runtime id. +struct IxiaLink { + std::string ixia_interface; + std::string sut_interface; + int port_id; +}; + +class SflowTestFixture : public testing::TestWithParam { protected: + void SetUp() override; + + void TearDown() override; + + const p4::config::v1::P4Info& GetP4Info() { return GetParam().p4_info; } + const pdpi::IrP4Info& GetIrP4Info() { return ir_p4_info_; } + + std::unique_ptr testbed_; + pdpi::IrP4Info ir_p4_info_; + std::unique_ptr gnmi_stub_; + std::unique_ptr sut_p4_session_; + thinkit::SSHClient* ssh_client_ = GetParam().ssh_client; + + std::vector ready_links_; }; } // namespace pins