| // Copyright (c) 2019 The Chromium Authors. All rights reserved. | |
| // Use of this source code is governed by a BSD-style license that can be | |
| // found in the LICENSE file. | |
| #include "quiche/quic/qbone/qbone_packet_processor.h" | |
| #include <utility> | |
| #include "absl/strings/string_view.h" | |
| #include "quiche/quic/platform/api/quic_test.h" | |
| #include "quiche/quic/qbone/qbone_packet_processor_test_tools.h" | |
| #include "quiche/common/quiche_text_utils.h" | |
| namespace quic::test { | |
| namespace { | |
| using Direction = QbonePacketProcessor::Direction; | |
| using ProcessingResult = QbonePacketProcessor::ProcessingResult; | |
| using OutputInterface = QbonePacketProcessor::OutputInterface; | |
| using ::testing::_; | |
| using ::testing::Eq; | |
| using ::testing::Invoke; | |
| using ::testing::Return; | |
| using ::testing::WithArgs; | |
| // clang-format off | |
| static const char kReferenceClientPacketData[] = { | |
| // IPv6 with zero TOS and flow label. | |
| 0x60, 0x00, 0x00, 0x00, | |
| // Payload size is 8 bytes. | |
| 0x00, 0x08, | |
| // Next header is UDP | |
| 17, | |
| // TTL is 50. | |
| 50, | |
| // IP address of the sender is fd00:0:0:1::1 | |
| 0xfd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, | |
| 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, | |
| // IP address of the receiver is fd00:0:0:5::1 | |
| 0xfd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, | |
| 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, | |
| // Source port 12345 | |
| 0x30, 0x39, | |
| // Destination port 443 | |
| 0x01, 0xbb, | |
| // UDP content length is zero | |
| 0x00, 0x00, | |
| // Checksum is not actually checked in any of the tests, so we leave it as | |
| // zero | |
| 0x00, 0x00, | |
| }; | |
| static const char kReferenceNetworkPacketData[] = { | |
| // IPv6 with zero TOS and flow label. | |
| 0x60, 0x00, 0x00, 0x00, | |
| // Payload size is 8 bytes. | |
| 0x00, 0x08, | |
| // Next header is UDP | |
| 17, | |
| // TTL is 50. | |
| 50, | |
| // IP address of the sender is fd00:0:0:5::1 | |
| 0xfd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, | |
| 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, | |
| // IP address of the receiver is fd00:0:0:1::1 | |
| 0xfd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, | |
| 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, | |
| // Source port 443 | |
| 0x01, 0xbb, | |
| // Destination port 12345 | |
| 0x30, 0x39, | |
| // UDP content length is zero | |
| 0x00, 0x00, | |
| // Checksum is not actually checked in any of the tests, so we leave it as | |
| // zero | |
| 0x00, 0x00, | |
| }; | |
| static const char kReferenceClientSubnetPacketData[] = { | |
| // IPv6 with zero TOS and flow label. | |
| 0x60, 0x00, 0x00, 0x00, | |
| // Payload size is 8 bytes. | |
| 0x00, 0x08, | |
| // Next header is UDP | |
| 17, | |
| // TTL is 50. | |
| 50, | |
| // IP address of the sender is fd00:0:0:2::1, which is within the /62 of the | |
| // client. | |
| 0xfd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, | |
| 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, | |
| // IP address of the receiver is fd00:0:0:5::1 | |
| 0xfd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, | |
| 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, | |
| // Source port 12345 | |
| 0x30, 0x39, | |
| // Destination port 443 | |
| 0x01, 0xbb, | |
| // UDP content length is zero | |
| 0x00, 0x00, | |
| // Checksum is not actually checked in any of the tests, so we leave it as | |
| // zero | |
| 0x00, 0x00, | |
| }; | |
| static const char kReferenceEchoRequestData[] = { | |
| // IPv6 with zero TOS and flow label. | |
| 0x60, 0x00, 0x00, 0x00, | |
| // Payload size is 64 bytes. | |
| 0x00, 64, | |
| // Next header is ICMP | |
| 58, | |
| // TTL is 127. | |
| 127, | |
| // IP address of the sender is fd00:0:0:1::1 | |
| 0xfd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, | |
| 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, | |
| // IP address of the receiver is fe80::71:626f:6e6f | |
| 0xfe, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, | |
| 0x00, 0x00, 0x00, 0x71, 0x62, 0x6f, 0x6e, 0x6f, | |
| // ICMP Type ping request | |
| 128, | |
| // ICMP Code 0 | |
| 0, | |
| // Checksum is not actually checked in any of the tests, so we leave it as | |
| // zero | |
| 0x00, 0x00, | |
| // ICMP Identifier (0xcafe to be memorable) | |
| 0xca, 0xfe, | |
| // Sequence number | |
| 0x00, 0x01, | |
| // Data, starting with unix timeval then 0x10..0x37 | |
| 0x67, 0x37, 0x8a, 0x63, 0x00, 0x00, 0x00, 0x00, | |
| 0x96, 0x58, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, | |
| 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, | |
| 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, | |
| 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, | |
| 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, | |
| 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, | |
| }; | |
| static const char kReferenceEchoReplyData[] = { | |
| // IPv6 with zero TOS and flow label. | |
| 0x60, 0x00, 0x00, 0x00, | |
| // Payload size is 64 bytes. | |
| 0x00, 64, | |
| // Next header is ICMP | |
| 58, | |
| // TTL is 255. | |
| 255, | |
| // IP address of the sender is fd00:4:0:1::1 | |
| 0xfd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, | |
| 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, | |
| // IP address of the receiver is fd00:0:0:1::1 | |
| 0xfd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, | |
| 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, | |
| // ICMP Type ping reply | |
| 129, | |
| // ICMP Code 0 | |
| 0, | |
| // Checksum | |
| 0x66, 0xb6, | |
| // ICMP Identifier (0xcafe to be memorable) | |
| 0xca, 0xfe, | |
| // Sequence number | |
| 0x00, 0x01, | |
| // Data, starting with unix timeval then 0x10..0x37 | |
| 0x67, 0x37, 0x8a, 0x63, 0x00, 0x00, 0x00, 0x00, | |
| 0x96, 0x58, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, | |
| 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, | |
| 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, | |
| 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, | |
| 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, | |
| 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, | |
| }; | |
| // clang-format on | |
| static const absl::string_view kReferenceClientPacket( | |
| kReferenceClientPacketData, ABSL_ARRAYSIZE(kReferenceClientPacketData)); | |
| static const absl::string_view kReferenceNetworkPacket( | |
| kReferenceNetworkPacketData, ABSL_ARRAYSIZE(kReferenceNetworkPacketData)); | |
| static const absl::string_view kReferenceClientSubnetPacket( | |
| kReferenceClientSubnetPacketData, | |
| ABSL_ARRAYSIZE(kReferenceClientSubnetPacketData)); | |
| static const absl::string_view kReferenceEchoRequest( | |
| kReferenceEchoRequestData, ABSL_ARRAYSIZE(kReferenceEchoRequestData)); | |
| MATCHER_P(IsIcmpMessage, icmp_type, | |
| "Checks whether the argument is an ICMP message of supplied type") { | |
| if (arg.size() < kTotalICMPv6HeaderSize) { | |
| return false; | |
| } | |
| return arg[40] == icmp_type; | |
| } | |
| class MockPacketFilter : public QbonePacketProcessor::Filter { | |
| public: | |
| MOCK_METHOD(ProcessingResult, FilterPacket, | |
| (Direction, absl::string_view, absl::string_view, icmp6_hdr*, | |
| OutputInterface*), | |
| (override)); | |
| }; | |
| class QbonePacketProcessorTest : public QuicTest { | |
| protected: | |
| QbonePacketProcessorTest() { | |
| QUICHE_CHECK(client_ip_.FromString("fd00:0:0:1::1")); | |
| QUICHE_CHECK(self_ip_.FromString("fd00:0:0:4::1")); | |
| QUICHE_CHECK(network_ip_.FromString("fd00:0:0:5::1")); | |
| processor_ = std::make_unique<QbonePacketProcessor>( | |
| self_ip_, client_ip_, /*client_ip_subnet_length=*/62, &output_, | |
| &stats_); | |
| } | |
| void SendPacketFromClient(absl::string_view packet) { | |
| std::string packet_buffer(packet.data(), packet.size()); | |
| processor_->ProcessPacket(&packet_buffer, Direction::FROM_OFF_NETWORK); | |
| } | |
| void SendPacketFromNetwork(absl::string_view packet) { | |
| std::string packet_buffer(packet.data(), packet.size()); | |
| processor_->ProcessPacket(&packet_buffer, Direction::FROM_NETWORK); | |
| } | |
| QuicIpAddress client_ip_; | |
| QuicIpAddress self_ip_; | |
| QuicIpAddress network_ip_; | |
| std::unique_ptr<QbonePacketProcessor> processor_; | |
| testing::StrictMock<MockPacketProcessorOutput> output_; | |
| testing::StrictMock<MockPacketProcessorStats> stats_; | |
| }; | |
| TEST_F(QbonePacketProcessorTest, EmptyPacket) { | |
| EXPECT_CALL(stats_, OnPacketDroppedSilently(Direction::FROM_OFF_NETWORK)); | |
| SendPacketFromClient(""); | |
| EXPECT_CALL(stats_, OnPacketDroppedSilently(Direction::FROM_NETWORK)); | |
| SendPacketFromNetwork(""); | |
| } | |
| TEST_F(QbonePacketProcessorTest, RandomGarbage) { | |
| EXPECT_CALL(stats_, OnPacketDroppedSilently(Direction::FROM_OFF_NETWORK)); | |
| SendPacketFromClient(std::string(1280, 'a')); | |
| EXPECT_CALL(stats_, OnPacketDroppedSilently(Direction::FROM_NETWORK)); | |
| SendPacketFromNetwork(std::string(1280, 'a')); | |
| } | |
| TEST_F(QbonePacketProcessorTest, RandomGarbageWithCorrectLengthFields) { | |
| std::string packet(40, 'a'); | |
| packet[4] = 0; | |
| packet[5] = 0; | |
| EXPECT_CALL(stats_, OnPacketDroppedWithIcmp(Direction::FROM_OFF_NETWORK)); | |
| EXPECT_CALL(output_, SendPacketToClient(IsIcmpMessage(ICMP6_DST_UNREACH))); | |
| SendPacketFromClient(packet); | |
| } | |
| TEST_F(QbonePacketProcessorTest, GoodPacketFromClient) { | |
| EXPECT_CALL(stats_, OnPacketForwarded(Direction::FROM_OFF_NETWORK)); | |
| EXPECT_CALL(output_, SendPacketToNetwork(_)); | |
| SendPacketFromClient(kReferenceClientPacket); | |
| } | |
| TEST_F(QbonePacketProcessorTest, GoodPacketFromClientSubnet) { | |
| EXPECT_CALL(stats_, OnPacketForwarded(Direction::FROM_OFF_NETWORK)); | |
| EXPECT_CALL(output_, SendPacketToNetwork(_)); | |
| SendPacketFromClient(kReferenceClientSubnetPacket); | |
| } | |
| TEST_F(QbonePacketProcessorTest, GoodPacketFromNetwork) { | |
| EXPECT_CALL(stats_, OnPacketForwarded(Direction::FROM_NETWORK)); | |
| EXPECT_CALL(output_, SendPacketToClient(_)); | |
| SendPacketFromNetwork(kReferenceNetworkPacket); | |
| } | |
| TEST_F(QbonePacketProcessorTest, GoodPacketFromNetworkWrongDirection) { | |
| EXPECT_CALL(stats_, OnPacketDroppedWithIcmp(Direction::FROM_OFF_NETWORK)); | |
| EXPECT_CALL(output_, SendPacketToClient(IsIcmpMessage(ICMP6_DST_UNREACH))); | |
| SendPacketFromClient(kReferenceNetworkPacket); | |
| } | |
| TEST_F(QbonePacketProcessorTest, TtlExpired) { | |
| std::string packet(kReferenceNetworkPacket); | |
| packet[7] = 1; | |
| EXPECT_CALL(stats_, OnPacketDroppedWithIcmp(Direction::FROM_NETWORK)); | |
| EXPECT_CALL(output_, SendPacketToNetwork(IsIcmpMessage(ICMP6_TIME_EXCEEDED))); | |
| SendPacketFromNetwork(packet); | |
| } | |
| TEST_F(QbonePacketProcessorTest, UnknownProtocol) { | |
| std::string packet(kReferenceNetworkPacket); | |
| packet[6] = IPPROTO_SCTP; | |
| EXPECT_CALL(stats_, OnPacketDroppedWithIcmp(Direction::FROM_NETWORK)); | |
| EXPECT_CALL(output_, SendPacketToNetwork(IsIcmpMessage(ICMP6_PARAM_PROB))); | |
| SendPacketFromNetwork(packet); | |
| } | |
| TEST_F(QbonePacketProcessorTest, FilterFromClient) { | |
| auto filter = std::make_unique<MockPacketFilter>(); | |
| EXPECT_CALL(*filter, FilterPacket(_, _, _, _, _)) | |
| .WillRepeatedly(Return(ProcessingResult::SILENT_DROP)); | |
| processor_->set_filter(std::move(filter)); | |
| EXPECT_CALL(stats_, OnPacketDroppedSilently(Direction::FROM_OFF_NETWORK)); | |
| SendPacketFromClient(kReferenceClientPacket); | |
| } | |
| class TestFilter : public QbonePacketProcessor::Filter { | |
| public: | |
| TestFilter(QuicIpAddress client_ip, QuicIpAddress network_ip) | |
| : client_ip_(client_ip), network_ip_(network_ip) {} | |
| ProcessingResult FilterPacket(Direction direction, | |
| absl::string_view full_packet, | |
| absl::string_view payload, | |
| icmp6_hdr* icmp_header, | |
| OutputInterface* output) override { | |
| EXPECT_EQ(kIPv6HeaderSize, full_packet.size() - payload.size()); | |
| EXPECT_EQ(IPPROTO_UDP, TransportProtocolFromHeader(full_packet)); | |
| EXPECT_EQ(client_ip_, SourceIpFromHeader(full_packet)); | |
| EXPECT_EQ(network_ip_, DestinationIpFromHeader(full_packet)); | |
| called_++; | |
| return ProcessingResult::SILENT_DROP; | |
| } | |
| int called() const { return called_; } | |
| private: | |
| int called_ = 0; | |
| QuicIpAddress client_ip_; | |
| QuicIpAddress network_ip_; | |
| }; | |
| // Verify that the parameters are passed correctly into the filter, and that the | |
| // helper functions of the filter class work. | |
| TEST_F(QbonePacketProcessorTest, FilterHelperFunctions) { | |
| auto filter_owned = std::make_unique<TestFilter>(client_ip_, network_ip_); | |
| TestFilter* filter = filter_owned.get(); | |
| processor_->set_filter(std::move(filter_owned)); | |
| EXPECT_CALL(stats_, OnPacketDroppedSilently(Direction::FROM_OFF_NETWORK)); | |
| SendPacketFromClient(kReferenceClientPacket); | |
| ASSERT_EQ(1, filter->called()); | |
| } | |
| TEST_F(QbonePacketProcessorTest, Icmp6EchoResponseHasRightPayload) { | |
| auto filter = std::make_unique<MockPacketFilter>(); | |
| EXPECT_CALL(*filter, FilterPacket(_, _, _, _, _)) | |
| .WillOnce(WithArgs<2, 3>( | |
| Invoke([](absl::string_view payload, icmp6_hdr* icmp_header) { | |
| icmp_header->icmp6_type = ICMP6_ECHO_REPLY; | |
| icmp_header->icmp6_code = 0; | |
| auto* request_header = | |
| reinterpret_cast<const icmp6_hdr*>(payload.data()); | |
| icmp_header->icmp6_id = request_header->icmp6_id; | |
| icmp_header->icmp6_seq = request_header->icmp6_seq; | |
| return ProcessingResult::ICMP; | |
| }))); | |
| processor_->set_filter(std::move(filter)); | |
| EXPECT_CALL(stats_, OnPacketDroppedWithIcmp(Direction::FROM_OFF_NETWORK)); | |
| EXPECT_CALL(output_, SendPacketToClient(_)) | |
| .WillOnce(Invoke([](absl::string_view packet) { | |
| // Explicit conversion because otherwise it is treated as a null | |
| // terminated string. | |
| absl::string_view expected = absl::string_view( | |
| kReferenceEchoReplyData, sizeof(kReferenceEchoReplyData)); | |
| EXPECT_THAT(packet, Eq(expected)); | |
| QUIC_LOG(INFO) << "ICMP response:\n" | |
| << quiche::QuicheTextUtils::HexDump(packet); | |
| })); | |
| SendPacketFromClient(kReferenceEchoRequest); | |
| } | |
| } // namespace | |
| } // namespace quic::test |