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..a5bd282d 100644 --- a/tests/sflow/BUILD.bazel +++ b/tests/sflow/BUILD.bazel @@ -20,10 +20,60 @@ package( cc_library( name = "sflow_test", testonly = True, + srcs = ["sflow_test.cc"], hdrs = ["sflow_test.h"], deps = [ - "//thinkit:mirror_testbed_fixture", + ":sflow_util", + "//gutil:collections", + "//gutil:status_matchers", + "//gutil:testing", + "//lib:ixia_helper", + "//lib/gnmi:gnmi_helper", + "//lib/utils:json_utils", + "//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/forwarding:group_programming_util", + "//tests/forwarding:packet_test_util", + "//tests/forwarding:util", + "//tests/lib:p4rt_fixed_table_programming_helper", + "//tests/lib:switch_test_setup_helpers", + "//tests/qos:gnmi_parsers", + "//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/container:flat_hash_set", + "@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_absl//absl/types:span", + "@com_google_googletest//:gtest", ], alwayslink = True, ) + +cc_library( + name = "sflow_util", + testonly = True, + srcs = ["sflow_util.cc"], + hdrs = ["sflow_util.h"], + deps = [ + "//lib/gnmi:gnmi_helper", + "//lib/validator:validator_lib", + "@com_github_gnmi//proto/gnmi:gnmi_cc_grpc_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/strings", + "@com_google_absl//absl/time", + ], +) diff --git a/tests/sflow/sflow_test.cc b/tests/sflow/sflow_test.cc new file mode 100644 index 00000000..2d7198a9 --- /dev/null +++ b/tests/sflow/sflow_test.cc @@ -0,0 +1,1113 @@ +// 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/container/flat_hash_set.h" +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "absl/strings/numbers.h" +#include "absl/strings/str_cat.h" +#include "absl/strings/str_join.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 "absl/types/span.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/utils/json_utils.h" +#include "lib/validator/validator_lib.h" +#include "p4_pdpi/p4_runtime_session.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/forwarding/group_programming_util.h" +#include "tests/forwarding/packet_test_util.h" +#include "tests/forwarding/util.h" +#include "tests/lib/p4rt_fixed_table_programming_helper.h" +#include "tests/lib/switch_test_setup_helpers.h" +#include "tests/qos/gnmi_parsers.h" +#include "tests/sflow/sflow_util.h" +#include "thinkit/generic_testbed.h" +#include "thinkit/ssh_client.h" + +namespace pins { + +namespace { + +using ::gutil::IsOkAndHolds; + +// Number of packets sent to one port. +constexpr int kPacketsNum = 400000; + +// Number of packets sent per second. +constexpr int kPacketsPerSecond = 16000; + +// 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 kSflowtoolLineFormatTemplate = + "/etc/init.d/sflow-container exec '$0 -l -p 6343 &" + " pid=$$!; sleep $1; kill -9 $$pid;'"; + +constexpr absl::string_view kSflowtoolFullFormatTemplate = + "/etc/init.d/sflow-container exec '$0 -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, 03); +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.15; + +constexpr absl::string_view kSpeed100GB = + "\"openconfig-if-ethernet:SPEED_100GB\""; +constexpr absl::string_view kSpeed200GB = + "\"openconfig-if-ethernet:SPEED_200GB\""; + +// Vrf prefix used in the test. +constexpr absl::string_view kVrfIdPrefix = "vrf-"; + +// Returns IP address in dot-decimal notation, e.g. "192.168.2.1". +std::string GetSrcIpv4AddrByPortId(const int port_id) { + return netaddr::Ipv4Address(std::bitset<32>(kIpV4Src + port_id)).ToString(); +} + +// Sets ACL punt rule according to `port_id`. +absl::Status SetUpAclPunt(pdpi::P4RuntimeSession& p4_session, + const pdpi::IrP4Info& ir_p4info, int port_id) { + ASSIGN_OR_RETURN( + p4::v1::TableEntry pi_entry, + pdpi::PartialPdTableEntryToPiTableEntry( + ir_p4info, gutil::ParseProtoOrDie(absl::Substitute( + R"pb( + acl_ingress_table_entry { + match { + dst_mac { value: "$0" mask: "ff:ff:ff:ff:ff:ff" } + is_ipv4 { value: "0x1" } + src_ip { value: "$1" mask: "255.255.255.255" } + dst_ip { value: "$2" mask: "255.255.255.255" } + } + action { acl_trap { qos_queue: "0x7" } } + priority: 1 + } + )pb", + kDstMac.ToString(), GetSrcIpv4AddrByPortId(port_id), + kIpV4Dst.ToString())))); + return pdpi::InstallPiTableEntry(&p4_session, pi_entry); +} + +// Sets ACL drop rule according to `port_id`. +absl::Status SetUpAclDrop(pdpi::P4RuntimeSession& p4_session, + const pdpi::IrP4Info& ir_p4info, int port_id) { + ASSIGN_OR_RETURN( + p4::v1::TableEntry pi_entry, + pdpi::PartialPdTableEntryToPiTableEntry( + ir_p4info, gutil::ParseProtoOrDie(absl::Substitute( + R"pb( + acl_ingress_table_entry { + match { + dst_mac { value: "$0" mask: "ff:ff:ff:ff:ff:ff" } + is_ipv4 { value: "0x1" } + src_ip { value: "$1" mask: "255.255.255.255" } + dst_ip { value: "$2" mask: "255.255.255.255" } + } + action { acl_drop {} } + priority: 1 + } + )pb", + kDstMac.ToString(), GetSrcIpv4AddrByPortId(port_id), + kIpV4Dst.ToString())))); + return pdpi::InstallPiTableEntry(&p4_session, pi_entry); +} + +// Sets VRF according to port number. The pattern would be vrf-x (x=port id). +absl::Status SetSutVrf(pdpi::P4RuntimeSession& p4_session, + const p4::config::v1::P4Info& p4info, + const pdpi::IrP4Info& ir_p4info, + absl::Span port_ids) { + for (int i = 0; i < port_ids.size(); ++i) { + // Create default VRF for test. + ASSIGN_OR_RETURN( + p4::v1::TableEntry pi_entry, + pdpi::PartialPdTableEntryToPiTableEntry( + ir_p4info, gutil::ParseProtoOrDie(absl::Substitute( + R"pb( + vrf_table_entry { + match { vrf_id: "$0" } + action { no_action {} } + })pb", + absl::StrCat(kVrfIdPrefix, port_ids[i]))))); + RETURN_IF_ERROR(pdpi::InstallPiTableEntry(&p4_session, pi_entry)); + + ASSIGN_OR_RETURN( + pi_entry, + pdpi::PartialPdTableEntryToPiTableEntry( + ir_p4info, + gutil::ParseProtoOrDie(absl::Substitute( + R"pb( + acl_pre_ingress_table_entry { + match { in_port { value: "$0" } } # Match in port + action { set_vrf { vrf_id: "$1" } } + priority: 1 + })pb", + port_ids[i], absl::StrCat(kVrfIdPrefix, port_ids[i]))))); + RETURN_IF_ERROR(pdpi::InstallPiTableEntry(&p4_session, pi_entry)); + } + + return absl::OkStatus(); +} + +// Creates members by filling in the controller port ids. +absl::StatusOr> CreateGroupMembers( + int group_size, absl::Span controller_port_ids) { + if (group_size > controller_port_ids.size()) { + return absl::InvalidArgumentError( + absl::StrCat("Not enough members: ", controller_port_ids.size(), + " to create the group with size: ", group_size)); + } + std::vector members(group_size); + for (int i = 0; i < group_size; i++) { + // Skip weight since we don't need it in this test. + members[i] = pins::GroupMember{.port = controller_port_ids[i]}; + LOG(INFO) << "member-" << i << " port: " << members[i].port; + } + return members; +} + +// Program route entries using vrf_id. +absl::Status ProgramRoutes(pdpi::P4RuntimeSession& p4_session, + const pdpi::IrP4Info& ir_p4info, const int port_id, + absl::string_view next_hop_id) { + const std::string vrf_id = absl::StrCat(kVrfIdPrefix, port_id); + // Add set of flows to allow forwarding. + auto ipv4_entry = gutil::ParseProtoOrDie(absl::Substitute( + R"pb( + type: INSERT + table_entry { + ipv4_table_entry { + match { vrf_id: "$0" } + action { set_nexthop_id { nexthop_id: "$1" } } + } + })pb", + vrf_id, next_hop_id)); + p4::v1::WriteRequest write_request; + ASSIGN_OR_RETURN( + p4::v1::Update pi_entry, pdpi::PdUpdateToPi(ir_p4info, ipv4_entry), + _.SetPrepend() << "Failed in PD table conversion to PI, entry: " + << ipv4_entry.DebugString() << " error: "); + *write_request.add_updates() = pi_entry; + return pdpi::SetMetadataAndSendPiWriteRequest(&p4_session, write_request); +} + +// Program L3 Admit table for the given mac-address. +absl::Status ProgramL3Admit(pdpi::P4RuntimeSession& p4_session, + const pdpi::IrP4Info& ir_p4info, + const L3AdmitOptions& options) { + p4::v1::WriteRequest write_request; + ASSIGN_OR_RETURN( + *write_request.add_updates(), + L3AdmitTableUpdate(ir_p4info, p4::v1::Update::INSERT, options)); + return pdpi::SetMetadataAndSendPiWriteRequest(&p4_session, write_request); +} + +// 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, + absl::string_view sflow_template, const int sflowtool_runtime, + std::string& sflow_tool_result) { + std::thread sflow_tool_thread = std::thread([&sflow_tool_result, ssh_client, + device_name, sflow_template, + sflowtool_runtime]() { + const std::string ssh_command = + absl::Substitute(sflow_template, 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(absl::Span traffic_refs, + absl::string_view topology_ref, + absl::Span ixia_links, + thinkit::GenericTestbed& testbed, + gnmi::gNMI::StubInterface* gnmi_stub, + const int pkt_count, const int pkt_rate) { + // 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 * pkt_count / pkt_rate)), + testbed)); + + 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: " + << ". Interface: " << ixia_links[i].sut_interface << ". 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\""; +} + +struct Port { + std::string interface_name; + int port_id; +}; + +// Returns an available port which is UP and different from `ingress_port`. +// Returns an error if failed. +absl::StatusOr GetUpEgressPort(thinkit::GenericTestbed& generic_testbed, + gnmi::gNMI::StubInterface& gnmi_stub, + absl::string_view ingress_port) { + absl::flat_hash_map port_id_per_port_name; + ASSIGN_OR_RETURN(port_id_per_port_name, + pins_test::GetAllUpInterfacePortIdsByName(gnmi_stub)); + for (const auto& [interface, port_id_str] : port_id_per_port_name) { + int port_id; + if (interface != ingress_port && absl::SimpleAtoi(port_id_str, &port_id)) { + return Port{ + .interface_name = interface, + .port_id = port_id, + }; + } + } + return absl::FailedPreconditionError("No more usable port ids."); +} + +// 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 rule; + std::string sut_interface; + int packets; + int sampling_rate; + int expected_samples; + int actual_samples; + + std::string DebugString() { + return absl::Substitute( + "Rule: $0\n" + "Ingress interface: $1\n" + "Total packets input: $2\n" + "Sampling rate: 1 in $3\n" + "Expected samples: $4\n" + "Actual samples: $5", + rule, sut_interface, packets, sampling_rate, expected_samples, + actual_samples); + } +}; + +absl::StatusOr GenerateSflowConfig( + thinkit::GenericTestbed* testbed, const std::string& gnmi_config) { + ASSIGN_OR_RETURN(auto loopback_ipv6, + pins_test::ParseLoopbackIpv6s(gnmi_config)); + + if (loopback_ipv6.empty()) { + return absl::FailedPreconditionError(absl::Substitute( + "No loopback IP found for $0 testbed.", testbed->Sut().ChassisName())); + } + + absl::flat_hash_map interface_info = + testbed->GetSutInterfaceInfo(); + absl::flat_hash_set sflow_interface_names; + for (const auto& [interface, info] : interface_info) { + if (info.interface_modes.contains(thinkit::CONTROL_INTERFACE) || + info.interface_modes.contains(thinkit::TRAFFIC_GENERATOR)) { + sflow_interface_names.insert(interface); + } + } + + return absl::OkStatus(); +} + +// Returns the value of `key` in `sflow_datagram`. Returns an error if not +// found. +absl::StatusOr ExtractValueByKey( + absl::string_view sflow_datagram, absl::string_view key) { + int idx = sflow_datagram.find(key); + if (idx == std::string::npos) { + return absl::NotFoundError( + absl::Substitute("Cannot find $0 in $1", key, sflow_datagram)); + } + return absl::StripAsciiWhitespace(sflow_datagram.substr( + idx + key.size(), sflow_datagram.find('\n', idx) - idx - key.size())); +} + +// Returns the headerLen from `sflowtool_output`. Also stores datagram into test +// artifact with `file_name`. Returns -1 if any error occurs. +absl::StatusOr GetHeaderLenFromSflowOutput( + absl::string_view sflowtool_output, int port_id, + absl::string_view file_name, thinkit::TestEnvironment& test_environment) { + // Every "startDatagram" indicates an sFlow datagram. + constexpr char kPattern[] = "startDatagram"; + size_t pos = sflowtool_output.rfind(kPattern, 0); + if (pos == std::string::npos) { + return absl::NotFoundError( + absl::Substitute("Cound not find $0 in sflowtool_output", kPattern)); + } + absl::string_view sflow_datagram = sflowtool_output.substr(pos); + LOG(INFO) << "sFlow datagram:\n" << sflow_datagram; + + // Example dump: + // startDatagram ================================= + // ... + // startSample ---------------------- + // ... + // headerLen 128 + // ... + // srcIP 1.2.5.12 + // ... + // endSample ---------------------- + // endDatagram ================================= + EXPECT_OK(test_environment.StoreTestArtifact(file_name, sflow_datagram)); + + // Verifies this datagram is generated from specific traffic. We use srcIP + // value for specific in port so we use it to validate as well. + ASSIGN_OR_RETURN(absl::string_view src_ip, + ExtractValueByKey(sflow_datagram, "srcIP")); + if (src_ip != GetSrcIpv4AddrByPortId(port_id)) { + return absl::FailedPreconditionError( + absl::Substitute("srcIp field in sflow sample is not as expected: " + "expected: $0, actual: $1.", + GetSrcIpv4AddrByPortId(port_id), src_ip)); + } + // Header length would be after `headerLen` field. + ASSIGN_OR_RETURN(absl::string_view header_len_str, + ExtractValueByKey(sflow_datagram, "headerLen")); + int header_len; + (void)absl::SimpleAtoi(header_len_str, &header_len); + return header_len; +} + +} // 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_AND_ASSIGN(auto gnmi_config_with_sflow, + GenerateSflowConfig(testbed_.get(), gnmi_config)); + ASSERT_OK(testbed_->Environment().StoreTestArtifact( + "gnmi_config_without_sflow.txt", gnmi_config)); + ASSERT_OK(testbed_->Environment().StoreTestArtifact( + "gnmi_config_with_sflow.txt", + json_yang::FormatJsonBestEffort(gnmi_config_with_sflow))); + ASSERT_OK(testbed_->Environment().StoreTestArtifact( + "p4info.pb.txt", GetP4Info().DebugString())); + ASSERT_OK_AND_ASSIGN( + sut_p4_session_, + pins_test::ConfigureSwitchAndReturnP4RuntimeSession( + testbed_->Sut(), gnmi_config_with_sflow, 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, VerifyIngressSamplingForNoMatchPackets) { + const IxiaLink& ingress_link = ready_links_[0]; + Port ingress_port = Port{ + .interface_name = ingress_link.sut_interface, + .port_id = ingress_link.port_id, + }; + + // 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({ingress_link}, *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(), + kSflowtoolLineFormatTemplate, + /*sflowtool_runtime=*/kPacketsNum / kPacketsPerSecond + 30, + sflow_result)); + + // Send packets from Ixia to SUT. + ASSERT_OK(SendSflowTraffic(ixia_ref_pair.first, ixia_ref_pair.second, + {ingress_link}, *testbed_, gnmi_stub_.get(), + kPacketsNum, kPacketsPerSecond)); + + // Wait for sflowtool to finish. + if (sflow_tool_thread.joinable()) { + sflow_tool_thread.join(); + } + LOG(INFO) << "sFlow samples:\n" << sflow_result; + + // Verify sflowtool result. Since we use port id to generate packets, we use + // port id to filter sFlow packets. + const int sample_count = + GetSflowSamplesOnSut(sflow_result, ingress_port.port_id); + const double expected_count = 1.0 * kPacketsNum / kSamplingRateInterval; + SflowResult result = SflowResult{ + .sut_interface = ingress_port.interface_name, + .packets = kPacketsNum, + .sampling_rate = kSamplingRateInterval, + .expected_samples = static_cast(expected_count), + .actual_samples = sample_count, + }; + LOG(INFO) << "------ Test result ------\n" << result.DebugString(); + EXPECT_GE(sample_count, expected_count * (1 - kTolerance)); + EXPECT_LE(sample_count, expected_count * (1 + kTolerance)); +} + +// Verifies ingress sampling could work when forwarding traffic. +TEST_P(SflowTestFixture, VerifyIngressSamplingForForwardedPackets) { + const IxiaLink& ingress_link = ready_links_[0]; + Port ingress_port = Port{ + .interface_name = ingress_link.sut_interface, + .port_id = ingress_link.port_id, + }; + ASSERT_OK_AND_ASSIGN( + Port egress_port, + GetUpEgressPort(*testbed_, *gnmi_stub_, ingress_port.interface_name)); + // Programs forwarding rule. + ASSERT_OK_AND_ASSIGN(std::vector members, + CreateGroupMembers(1, {egress_port.port_id})); + ASSERT_OK(pins::ProgramNextHops(testbed_->Environment(), *sut_p4_session_, + GetIrP4Info(), members)); + const std::string& egress_next_hop_id = members[0].nexthop; + ASSERT_OK(SetSutVrf(*sut_p4_session_, GetP4Info(), GetIrP4Info(), + {ingress_port.port_id})); + + // Allow the destination mac address to L3. + ASSERT_OK(ProgramL3Admit( + *sut_p4_session_, ir_p4_info_, + L3AdmitOptions{ + .priority = 2070, + .dst_mac = std::make_pair(kDstMac.ToString(), "FF:FF:FF:FF:FF:FF"), + })); + ASSERT_OK(ProgramRoutes(*sut_p4_session_, GetIrP4Info(), ingress_port.port_id, + egress_next_hop_id)); + + // Set up Ixia traffic. 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; + ASSERT_OK_AND_ASSIGN(ixia_ref_pair, + SetUpIxiaTraffic({ingress_link}, *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(), + kSflowtoolLineFormatTemplate, + /*sflowtool_runtime=*/kPacketsNum / kPacketsPerSecond + 10, + sflow_result)); + + // Send packets via Ixia. + ASSERT_OK(SendSflowTraffic(ixia_ref_pair.first, ixia_ref_pair.second, + {ingress_link}, *testbed_, gnmi_stub_.get(), + kPacketsNum, kPacketsPerSecond)); + + // Wait for sflowtool to finish. + if (sflow_tool_thread.joinable()) { + sflow_tool_thread.join(); + } + LOG(INFO) << "sFlow samples:\n" << sflow_result; + + // Verify sflowtool result. Since we use port id to generate packets, we use + // port id to filter sFlow packets. + const int sample_count = + GetSflowSamplesOnSut(sflow_result, ingress_port.port_id); + const double expected_count = 1.0 * kPacketsNum / kSamplingRateInterval; + EXPECT_GE(sample_count, expected_count * (1 - kTolerance)); + EXPECT_LE(sample_count, expected_count * (1 + kTolerance)); + SflowResult result = SflowResult{ + .rule = "Forward traffic", + .sut_interface = ingress_port.interface_name, + .packets = kPacketsNum, + .sampling_rate = kSamplingRateInterval, + .expected_samples = static_cast(expected_count), + .actual_samples = sample_count, + }; + LOG(INFO) << "------ Test result ------\n" << result.DebugString(); +} + +// Verifies ingress sampling could work when dropping packets. +TEST_P(SflowTestFixture, VerifyIngressSamplesForDropPackets) { + const IxiaLink& ingress_link = ready_links_[0]; + Port ingress_port = Port{ + .interface_name = ingress_link.sut_interface, + .port_id = ingress_link.port_id, + }; + ASSERT_OK( + SetUpAclDrop(*sut_p4_session_, GetIrP4Info(), ingress_port.port_id)); + + // Set up Ixia traffic. 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; + ASSERT_OK_AND_ASSIGN(ixia_ref_pair, + SetUpIxiaTraffic({ingress_link}, *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(), + kSflowtoolLineFormatTemplate, + /*sflowtool_runtime=*/kPacketsNum / kPacketsPerSecond + 10, + sflow_result)); + + // Send packets via Ixia. + ASSERT_OK(SendSflowTraffic(ixia_ref_pair.first, ixia_ref_pair.second, + {ingress_link}, *testbed_, gnmi_stub_.get(), + kPacketsNum, kPacketsPerSecond)); + + // Wait for sflowtool to finish. + if (sflow_tool_thread.joinable()) { + sflow_tool_thread.join(); + } + LOG(INFO) << "sFlow samples:\n" << sflow_result; + + // Verify sflowtool result. Since we use port id to generate packets, we use + // port id to filter sFlow packets. + const int sample_count = + GetSflowSamplesOnSut(sflow_result, ingress_port.port_id); + const double expected_count = 1.0 * kPacketsNum / kSamplingRateInterval; + EXPECT_GE(sample_count, expected_count * (1 - kTolerance)); + EXPECT_LE(sample_count, expected_count * (1 + kTolerance)); + SflowResult result = SflowResult{ + .rule = "Drop traffic", + .sut_interface = ingress_port.interface_name, + .packets = kPacketsNum, + .sampling_rate = kSamplingRateInterval, + .expected_samples = static_cast(expected_count), + .actual_samples = sample_count, + }; + LOG(INFO) << "------ Test result ------\n" << result.DebugString(); +} + +// TODO: Add a punt test case for cpu bound punt traffic like +// ssh/scp traffic: generate traffic from Ixia destined to Loopback0 ip addr and +// tcp port matching scp/ssh. +// Verifies ingress sampling could work when punting traffic. +TEST_P(SflowTestFixture, DISABLED_VerifyIngressSamplesForP4rtPuntTraffic) { + const IxiaLink& ingress_link = ready_links_[0]; + Port ingress_port = Port{ + .interface_name = ingress_link.sut_interface, + .port_id = ingress_link.port_id, + }; + ASSERT_OK( + SetUpAclPunt(*sut_p4_session_, GetIrP4Info(), ingress_port.port_id)); + + // Set up Ixia traffic. 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; + ASSERT_OK_AND_ASSIGN(ixia_ref_pair, + SetUpIxiaTraffic({ingress_link}, *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(), + kSflowtoolLineFormatTemplate, + /*sflowtool_runtime=*/kPacketsNum / kPacketsPerSecond + 10, + sflow_result)); + + // Send packets via Ixia. + ASSERT_OK(SendSflowTraffic(ixia_ref_pair.first, ixia_ref_pair.second, + {ingress_link}, *testbed_, gnmi_stub_.get(), + kPacketsNum, kPacketsPerSecond)); + // Wait for sflowtool to finish. + if (sflow_tool_thread.joinable()) { + sflow_tool_thread.join(); + } + LOG(INFO) << "sFlow samples:\n" << sflow_result; + + // Verify sflowtool result. Since we use port id to generate packets, we use + // port id to filter sFlow packets. + const int sample_count = + GetSflowSamplesOnSut(sflow_result, ingress_port.port_id); + const double expected_count = 1.0 * kPacketsNum / kSamplingRateInterval; + EXPECT_GE(sample_count, expected_count * (1 - kTolerance)); + EXPECT_LE(sample_count, expected_count * (1 + kTolerance)); + + SflowResult result = SflowResult{ + .rule = "Punt traffic", + .sut_interface = ingress_port.interface_name, + .packets = kPacketsNum, + .sampling_rate = kSamplingRateInterval, + .expected_samples = static_cast(expected_count), + .actual_samples = sample_count, + }; + LOG(INFO) << "------ Test result ------\n" << result.DebugString(); +} + +// Verifies sampling size could work: +// Traffic packet size size_a, sFlow sampling size size_b: expects sample header +// size equals to min(size_a, size_b). +TEST_P(SampleSizeTest, VerifySamplingSizeWorks) { + const int packet_size = GetParam().packet_size, + sample_size = GetParam().sample_size; + ASSERT_NE(packet_size, 0); + ASSERT_NE(sample_size, 0); + // 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; + ASSERT_OK(SetSflowSamplingSize(gnmi_stub_.get(), sample_size)); + const IxiaLink& ingress_link = ready_links_[0]; + + // Set up Ixia traffic. + ASSERT_OK_AND_ASSIGN(ixia_ref_pair, + SetUpIxiaTraffic({ingress_link}, *testbed_, kPacketsNum, + kPacketsPerSecond, packet_size)); + + // Start sflowtool on SUT. + std::string sflow_result; + ASSERT_OK_AND_ASSIGN( + std::thread sflow_tool_thread, + StartSflowCollector( + ssh_client_, testbed_->Sut().ChassisName(), + kSflowtoolFullFormatTemplate, + /*sflowtool_runtime=*/kPacketsNum / kPacketsPerSecond + 10, + sflow_result)); + + // Send packets with kPacketSize from Ixia to SUT. + ASSERT_OK(SendSflowTraffic(ixia_ref_pair.first, ixia_ref_pair.second, + {ingress_link}, *testbed_, gnmi_stub_.get(), + kPacketsNum, kPacketsPerSecond)); + + // Wait for sflowtool to finish. + if (sflow_tool_thread.joinable()) { + sflow_tool_thread.join(); + } + LOG(INFO) << "sFlow samples with sampling size " << kSampleSize << ":\n" + << sflow_result; + EXPECT_OK(testbed_->Environment().StoreTestArtifact("sflow_result.txt", + sflow_result)); + EXPECT_THAT( + GetHeaderLenFromSflowOutput( + sflow_result, ingress_link.port_id, + absl::Substitute("sFLow_datagram_packet_size_$0_sampling_size_$1.txt", + packet_size, sample_size), + testbed_->Environment()), + IsOkAndHolds(std::min( + sample_size, + packet_size - 4))); // sFlow would strip some bytes from each packet. +} + +} // namespace pins diff --git a/tests/sflow/sflow_test.h b/tests/sflow/sflow_test.h index fd461cd3..8aa0d44b 100644 --- a/tests/sflow/sflow_test.h +++ b/tests/sflow/sflow_test.h @@ -1,15 +1,74 @@ +// 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; + // For sampling size tests. + int packet_size; + int sample_size; +}; + +// 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_; }; +class SampleSizeTest : public SflowTestFixture {}; + } // namespace pins #endif // PINS_TESTS_SFLOW_SFLOW_TEST_H_ diff --git a/tests/sflow/sflow_util.cc b/tests/sflow/sflow_util.cc new file mode 100644 index 00000000..5b04b28c --- /dev/null +++ b/tests/sflow/sflow_util.cc @@ -0,0 +1,64 @@ +// 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_util.h" + +#include "absl/status/status.h" +#include "absl/strings/substitute.h" +#include "absl/time/time.h" +#include "gutil/status.h" +#include "lib/gnmi/gnmi_helper.h" +#include "lib/validator/validator_lib.h" + +namespace pins { +namespace { + +// --- Sflow gnmi config paths --- + +constexpr absl::string_view kSflowGnmiConfigSampleSizePath = + "/sampling/sflow/config/sample-size"; + +// --- Sflow gnmi state paths --- + +constexpr absl::string_view kSflowGnmiStateSampleSizePath = + "/sampling/sflow/state/sample-size"; + +} // namespace + +absl::Status VerifyGnmiStateConverged(gnmi::gNMI::StubInterface* gnmi_stub, + absl::string_view state_path, + absl::string_view expected_value) { + ASSIGN_OR_RETURN(std::string state_value, + pins_test::GetGnmiStatePathInfo(gnmi_stub, state_path)); + if (expected_value == state_value) { + return absl::OkStatus(); + } + return absl::FailedPreconditionError( + absl::StrCat(state_path, " value is ", state_value, + ", which is not equal to ", expected_value)); +} + +absl::Status SetSflowSamplingSize(gnmi::gNMI::StubInterface* gnmi_stub, + int sampling_size, absl::Duration timeout) { + std::string ops_val = absl::StrCat( + "{\"openconfig-sampling-sflow:sample-size\":", sampling_size, "}"); + + RETURN_IF_ERROR(SetGnmiConfigPath(gnmi_stub, kSflowGnmiConfigSampleSizePath, + pins_test::GnmiSetType::kUpdate, ops_val)); + + return pins_test::WaitForCondition(VerifyGnmiStateConverged, timeout, + gnmi_stub, kSflowGnmiStateSampleSizePath, + ops_val); +} +} // namespace pins diff --git a/tests/sflow/sflow_util.h b/tests/sflow/sflow_util.h new file mode 100644 index 00000000..7eefb169 --- /dev/null +++ b/tests/sflow/sflow_util.h @@ -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. + +#ifndef PINS_TESTS_SFLOW_SFLOW_UTIL_H_ +#define PINS_TESTS_SFLOW_SFLOW_UTIL_H_ + +#include "absl/container/flat_hash_map.h" +#include "absl/container/flat_hash_set.h" +#include "absl/status/status.h" +#include "absl/strings/string_view.h" +#include "proto/gnmi/gnmi.grpc.pb.h" + +namespace pins { + +// Reads value from `state_path` and verifies it is the same with +// `expected_value`. Returns a FailedPreconditionError if not matched. +absl::Status VerifyGnmiStateConverged(gnmi::gNMI::StubInterface* gnmi_stub, + absl::string_view state_path, + absl::string_view expected_value); + +// Sets sFLow sampling size to `sampling_size` and checks if it's applied to +// corresponding state path in `timeout`. Returns error if failed. +absl::Status SetSflowSamplingSize(gnmi::gNMI::StubInterface* gnmi_stub, + int sampling_size, + absl::Duration timeout = absl::Seconds(5)); + +} // namespace pins +#endif // PINS_TESTS_SFLOW_SFLOW_UTIL_H_