blob: 1d568e718b85718246d29862efb82cdf23447178 [file] [log] [blame]
// Copyright 2023 The ChromiumOS Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "routing-simulator/route_manager.h"
#include <string>
#include <string_view>
#include <base/check.h>
#include <base/logging.h>
#include <base/strings/string_split.h>
#include <chromeos/net-base/ip_address.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include "routing-simulator/mock_process_executor.h"
#include "routing-simulator/routing_decision_result.h"
#include "routing-simulator/routing_policy_entry.h"
#include "routing-simulator/routing_table.h"
namespace routing_simulator {
namespace {
using ::testing::Return;
constexpr std::string_view kMockIpRuleOutputIpv4 = R"(0: from all lookup local
1005: from 192.168.200.0/24 lookup 1001
1010: from all fwmark 0x3ea0000/0xffff0000 lookup 1002
1010: from 100.87.84.132/24 lookup 1002
1010: from all iif eth0 lookup 1002
1020: from all fwmark 0x3eb0000/0xffff0000 lookup 1003
1020: from all oif wlan0 lookup 1003
1020: from 100.86.210.153/22 lookup 1003
1020: from all iif wlan0 lookup 1003
32763: from 100.115.92.24/29 lookup 249
32763: from 100.115.92.32/27 lookup 249
32763: from 100.115.92.192/26 lookup 249
32765: from all lookup 1002
32766: from all lookup main
32767: from all lookup default)";
constexpr std::string_view kMockIpRuleOutputIpv6 = R"(0: from all lookup local
1000: from all lookup main
1010: from all fwmark 0x3ea0000/0xffff0000 lookup 1002
1010: from 2401:fa00:480:ee08:20e:c6ff:fe63:5c3f/64 lookup 1002
1010: from all iif eth0 lookup 1002
1020: from all oif wlan0 lookup 1003
1020: from 2a00:79e1:abc:f604:faac:65ff:fe56:100d/64 lookup 1003
1020: from 2a00:79e1:abc:f604:41d0:1fad:f561:15d8/64 lookup 1003
1020: from all iif wlan0 lookup 1003
32765: from all lookup 1002
32766: from all lookup main)";
constexpr std::string_view kMockIpRouteOutputIpv4 =
R"(default via 100.86.211.254 dev wlan0 table 1003 metric 65536
unreachable default table 250
192.168.200.0/24 dev wlan0 table 1001
throw 192.168.200.1 table 1001
100.86.208.0/22 dev wlan0 proto kernel scope link src 100.86.210.153
100.115.92.132/30 dev arc_ns1 proto kernel scope link src 100.115.92.133
local 100.86.210.153 dev wlan0 table local proto kernel scope host src 100.86.210.153
broadcast 100.86.211.255 dev wlan0 table local proto kernel scope link src 100.86.210.153)";
constexpr std::string_view kMockIpRouteOutputIpv6 =
R"(2401:fa00:480:ee08::/64 dev eth0 table 1002 proto kernel metric 256 expires 2591823sec pref medium
2a00:79e1:abc:f604::/64 dev wlan0 table 1003 proto kernel metric 256 expires 2591735sec pref medium
default via fe80::2a00:79e1:abc:f604 dev wlan0 table 1003 proto ra metric 1024 expires 3335sec hoplimit 64 pref medium
unreachable default dev lo table 250 metric 1024 pref medium
fdb9:72a:70c5:959d::/64 dev arc_ns1 proto kernel metric 256 pref medium
local ::1 dev lo table local proto kernel metric 0 pref medium
anycast 2401:fa00:480:ee08:: dev eth0 table local proto kernel metric 0 pref medium
multicast ff00::/8 dev wlan0 table local proto kernel metric 256 pref medium)";
const std::map<std::string, std::vector<std::string_view>>
kExpectedTableToRoutesIpv4 = {
{"1001",
{"192.168.200.0/24 dev wlan0 table 1001",
"throw 192.168.200.1 table 1001"}},
{"1003",
{"default via 100.86.211.254 dev wlan0 table 1003 metric 65536"}},
{"250", {"unreachable default table 250"}},
{"main",
{"100.86.208.0/22 dev wlan0 proto kernel scope link src "
"100.86.210.153",
"100.115.92.132/30 dev arc_ns1 proto kernel scope link src "
"100.115.92.133"}},
{"local",
{"local 100.86.210.153 dev wlan0 table local proto kernel scope "
"host src 100.86.210.153",
"broadcast 100.86.211.255 dev wlan0 table local proto kernel "
"scope link src 100.86.210.153"}}};
const std::map<std::string, std::vector<std::string_view>>
kExpectedTableToRoutesIpv6 = {
{"1002",
{"2401:fa00:480:ee08::/64 dev eth0 table 1002 proto kernel metric "
"256 "
"expires 2591823sec pref medium"}},
{"1003",
{
"2a00:79e1:abc:f604::/64 dev wlan0 table 1003 proto kernel metric "
"256 expires 2591735sec pref medium",
"default via fe80::2a00:79e1:abc:f604 dev wlan0 table 1003 proto "
"ra metric 1024 expires 3335sec hoplimit 64 pref medium",
}},
{"main",
{"fdb9:72a:70c5:959d::/64 dev arc_ns1 proto kernel metric 256 pref "
"medium"}},
{"local",
{"local ::1 dev lo table local proto kernel metric 0 pref medium",
"anycast 2401:fa00:480:ee08:: dev eth0 table local proto kernel "
"metric 0 pref medium",
"multicast ff00::/8 dev wlan0 table local proto kernel metric 256 "
"pref medium"}},
{"250",
{"unreachable default dev lo table 250 metric 1024 pref medium"}}};
// Creates an expected result of routing policy table by BuildTables().
std::vector<RoutingPolicyEntry> CreateRoutingPolicyTable(
std::string_view policies, net_base::IPFamily ip_family) {
std::vector<RoutingPolicyEntry> routing_policy_table_expected;
const auto policy_lines = base::SplitStringPiece(
policies, "\n", base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY);
for (const auto& policy_str : policy_lines) {
const auto policy =
RoutingPolicyEntry::CreateFromPolicyString(policy_str, ip_family);
CHECK(policy.has_value());
routing_policy_table_expected.push_back(*policy);
}
return routing_policy_table_expected;
}
// Creates an expected result of routing table by BuildTables().
std::map<std::string, RoutingTable> CreateRoutingTable(
std::map<std::string, std::vector<std::string_view>> table_to_routes,
net_base::IPFamily ip_family) {
std::map<std::string, RoutingTable> routing_tables_expected;
for (const auto& routing_table_case : table_to_routes) {
const auto table_id = routing_table_case.first;
const auto routes = routing_table_case.second;
RoutingTable routing_table;
for (const auto& route_str : routes) {
const auto route = Route::CreateFromRouteString(route_str, ip_family);
CHECK(route.has_value());
routing_table.AddRoute(*route);
}
routing_tables_expected.emplace(table_id, routing_table);
}
return routing_tables_expected;
}
void VerifyMatchingResult(
const RoutingDecisionResult& actual,
const std::vector<std::string_view>& expected_policies,
const std::vector<std::string_view>& expected_routes) {
const auto result = actual.result();
CHECK(expected_policies.size() == expected_routes.size() &&
expected_policies.size() == result.size());
std::vector<std::string_view> actual_policies, actual_routes;
for (const auto& [policy, route] : result) {
actual_policies.push_back(policy->policy_str());
if (route == nullptr) {
actual_routes.push_back("");
} else {
actual_routes.push_back(route->route_str());
}
}
EXPECT_EQ(actual_policies, expected_policies);
EXPECT_EQ(actual_routes, expected_routes);
}
TEST(RouteManagerTest, BuildTablesTest) {
MockProcessExecutor process_executor;
// Execute 'ip -4 rule show'.
EXPECT_CALL(process_executor,
RunAndGetStdout(base::FilePath("/bin/ip"),
std::vector<std::string>{"-4", "rule", "show"}))
.WillOnce(Return(std::string(kMockIpRuleOutputIpv4)));
// Execute 'ip -6 rule show'.
EXPECT_CALL(process_executor,
RunAndGetStdout(base::FilePath("/bin/ip"),
std::vector<std::string>{"-6", "rule", "show"}))
.WillOnce(Return(std::string(kMockIpRuleOutputIpv6)));
// Execute 'ip -4 route show table all'.
EXPECT_CALL(process_executor,
RunAndGetStdout(base::FilePath("/bin/ip"),
std::vector<std::string>{"-4", "route", "show",
"table", "all"}))
.WillOnce(Return(std::string(kMockIpRouteOutputIpv4)));
// Execute 'ip -6 route show table all'.
EXPECT_CALL(process_executor,
RunAndGetStdout(base::FilePath("/bin/ip"),
std::vector<std::string>{"-6", "route", "show",
"table", "all"}))
.WillOnce(Return(std::string(kMockIpRouteOutputIpv6)));
RouteManager route_manager(&process_executor);
route_manager.BuildTables();
// Test a routing policy table for IPv4.
const auto routing_policy_table_ipv4_expected = CreateRoutingPolicyTable(
kMockIpRuleOutputIpv4, net_base::IPFamily::kIPv4);
EXPECT_EQ(route_manager.routing_policy_table_ipv4(),
routing_policy_table_ipv4_expected);
// Test a routing policy table for IPv6.
const auto routing_policy_table_ipv6_expected = CreateRoutingPolicyTable(
kMockIpRuleOutputIpv6, net_base::IPFamily::kIPv6);
EXPECT_EQ(route_manager.routing_policy_table_ipv6(),
routing_policy_table_ipv6_expected);
// Test a routing table for IPv4.
auto routing_tables_ipv4_expected =
CreateRoutingTable(kExpectedTableToRoutesIpv4, net_base::IPFamily::kIPv4);
for (const auto& [table_id, routing_table_actual] :
route_manager.routing_tables_ipv4()) {
ASSERT_TRUE(routing_tables_ipv4_expected.contains(table_id));
EXPECT_EQ(routing_table_actual, routing_tables_ipv4_expected[table_id]);
}
// Test a routing table for IPv6.
auto routing_tables_ipv6_expected =
CreateRoutingTable(kExpectedTableToRoutesIpv6, net_base::IPFamily::kIPv6);
for (const auto& [table_id, routing_table_actual] :
route_manager.routing_tables_ipv6()) {
ASSERT_TRUE(routing_tables_ipv6_expected.contains(table_id));
EXPECT_EQ(routing_table_actual, routing_tables_ipv6_expected[table_id]);
}
}
class ProcessPacketTest : public ::testing::Test {
protected:
ProcessPacketTest() : route_manager_(RouteManager(&process_executor_)) {
// Execute 'ip -4 rule show'.
EXPECT_CALL(process_executor_,
RunAndGetStdout(base::FilePath("/bin/ip"),
std::vector<std::string>{"-4", "rule", "show"}))
.WillOnce(Return(std::string(kMockIpRuleOutputIpv4)));
// Execute 'ip -6 rule show'.
EXPECT_CALL(process_executor_,
RunAndGetStdout(base::FilePath("/bin/ip"),
std::vector<std::string>{"-6", "rule", "show"}))
.WillOnce(Return(std::string(kMockIpRuleOutputIpv6)));
// Execute 'ip -4 route show table all'.
EXPECT_CALL(process_executor_,
RunAndGetStdout(base::FilePath("/bin/ip"),
std::vector<std::string>{"-4", "route", "show",
"table", "all"}))
.WillOnce(Return(std::string(kMockIpRouteOutputIpv4)));
// Execute 'ip -6 route show table all'.
EXPECT_CALL(process_executor_,
RunAndGetStdout(base::FilePath("/bin/ip"),
std::vector<std::string>{"-6", "route", "show",
"table", "all"}))
.WillOnce(Return(std::string(kMockIpRouteOutputIpv6)));
route_manager_.BuildTables();
}
MockProcessExecutor process_executor_;
RouteManager route_manager_;
};
// Test the case when a packet matches with a policy only source prefix of
// which is specified. Also, It can match multiple routes and the matched policy
// of the matched route is in the middle of the matched policies.
TEST_F(ProcessPacketTest, IPv4MatchedBySourceIP) {
const auto ip_family = net_base::IPFamily::kIPv4;
const auto destination_ip =
net_base::IPAddress::CreateFromString("100.115.92.131").value();
const auto source_ip =
net_base::IPAddress::CreateFromString("100.86.208.70").value();
auto packet =
Packet::CreatePacketForTesting(ip_family, Packet::Protocol::kIcmp,
destination_ip, source_ip, 0, 0, "eth1");
ASSERT_TRUE(packet);
const auto result = route_manager_.ProcessPacketWithMutation(*packet);
std::vector<std::string_view> expected_policy_results = {
"0: from all lookup local", "1020: from 100.86.210.153/22 lookup 1003"};
std::vector<std::string_view> expected_route_results = {
"", "default via 100.86.211.254 dev wlan0 table 1003 metric 65536"};
ASSERT_EQ(result.result().size(), expected_policy_results.size());
VerifyMatchingResult(result, expected_policy_results, expected_route_results);
}
// Test the case when a packet matches with a policy only input interface of
// which is specified.
TEST_F(ProcessPacketTest, IPv4MatchedByInputInterface) {
// Matches a policy by input interface.
// protocol: TCP
const auto ip_family = net_base::IPFamily::kIPv4;
const auto destination_ip =
net_base::IPAddress::CreateFromString("198.86.208.70").value();
const auto source_ip =
net_base::IPAddress::CreateFromString("168.25.25.0").value();
auto packet = Packet::CreatePacketForTesting(
ip_family, Packet::Protocol::kTcp, destination_ip, source_ip, 100, 200,
"wlan0");
ASSERT_TRUE(packet);
const auto result = route_manager_.ProcessPacketWithMutation(*packet);
std::vector<std::string_view> expected_policy_results = {
"0: from all lookup local", "1020: from all iif wlan0 lookup 1003"};
std::vector<std::string_view> expected_route_results = {
"", "default via 100.86.211.254 dev wlan0 table 1003 metric 65536"};
ASSERT_EQ(result.result().size(), expected_policy_results.size());
VerifyMatchingResult(result, expected_policy_results, expected_route_results);
}
// Test the case when a packet matches with a policy which source prefix is
// default and not specified.
TEST_F(ProcessPacketTest, IPv4MatchedByDefault) {
const auto ip_family = net_base::IPFamily::kIPv4;
const auto destination_ip =
net_base::IPAddress::CreateFromString("100.115.92.133").value();
const auto source_ip =
net_base::IPAddress::CreateFromString("168.25.25.4").value();
auto packet = Packet::CreatePacketForTesting(
ip_family, Packet::Protocol::kUdp, destination_ip, source_ip, 100, 200,
"eth1");
ASSERT_TRUE(packet);
const auto result = route_manager_.ProcessPacketWithMutation(*packet);
std::vector<std::string_view> expected_policy_results = {
"0: from all lookup local", "32765: from all lookup 1002",
"32766: from all lookup main"};
std::vector<std::string_view> expected_route_results = {
"", "",
"100.115.92.132/30 dev arc_ns1 proto kernel scope link src "
"100.115.92.133"};
ASSERT_EQ(result.result().size(), expected_policy_results.size());
VerifyMatchingResult(result, expected_policy_results, expected_route_results);
}
// Test the case when no matched route is found.
TEST_F(ProcessPacketTest, IPv4NoMatchedRoute) {
const auto ip_family = net_base::IPFamily::kIPv4;
const auto destination_ip =
net_base::IPAddress::CreateFromString("160.25.25.0").value();
const auto source_ip =
net_base::IPAddress::CreateFromString("168.25.25.90").value();
auto packet =
Packet::CreatePacketForTesting(ip_family, Packet::Protocol::kIcmp,
destination_ip, source_ip, 0, 0, "eth1");
ASSERT_TRUE(packet);
const auto result = route_manager_.ProcessPacketWithMutation(*packet);
std::vector<std::string_view> expected_policy_results = {
"0: from all lookup local", "32765: from all lookup 1002",
"32766: from all lookup main", "32767: from all lookup default"};
std::vector<std::string_view> expected_route_results = {"", "", "", ""};
ASSERT_EQ(result.result().size(), expected_policy_results.size());
VerifyMatchingResult(result, expected_policy_results, expected_route_results);
}
// Test that throw routes are handled properly.
TEST_F(ProcessPacketTest, IPv4ThrowRoute) {
const auto ip_family = net_base::IPFamily::kIPv4;
const auto protocol = Packet::Protocol::kIcmp;
// Source IP should be matched by the rule pointing to the routing table
// contains the throw route.
const auto src_ip =
net_base::IPAddress::CreateFromString("192.168.200.100").value();
// Will be matched by the throw route.
const auto dst_ip =
net_base::IPAddress::CreateFromString("192.168.200.1").value();
// So that this packet can be matched by the rule pointing to table 1003 which
// contains a default route.
const std::string_view in_if = "wlan0";
// Packet matched by the throw route.
auto packet = Packet::CreatePacketForTesting(ip_family, protocol, dst_ip,
src_ip, 0, 0, in_if)
.value();
const auto result = route_manager_.ProcessPacketWithMutation(packet);
const std::vector<std::string_view> expected_policy_results = {
"0: from all lookup local",
"1005: from 192.168.200.0/24 lookup 1001",
"1020: from all iif wlan0 lookup 1003",
};
const std::vector<std::string_view> expected_route_results = {
"",
"throw 192.168.200.1 table 1001",
"default via 100.86.211.254 dev wlan0 table 1003 metric 65536",
};
ASSERT_EQ(result.result().size(), expected_policy_results.size());
VerifyMatchingResult(result, expected_policy_results, expected_route_results);
}
// Test the case when a packet matches with a policy only source prefix of
// which is specified. Also, It can match multiple routes and the matched policy
// of the matched route is in the middle of the matched policies.
TEST_F(ProcessPacketTest, IPv6MatchedBySourceIP) {
const auto ip_family = net_base::IPFamily::kIPv6;
const auto destination_ip =
net_base::IPAddress::CreateFromString("2401:fa00:480:ee08:300::").value();
const auto source_ip =
net_base::IPAddress::CreateFromString("2a00:79e1:abc:f604:200::").value();
auto packet =
Packet::CreatePacketForTesting(ip_family, Packet::Protocol::kIcmp,
destination_ip, source_ip, 0, 0, "eth1");
ASSERT_TRUE(packet);
const auto result = route_manager_.ProcessPacketWithMutation(*packet);
std::vector<std::string_view> expected_policy_results = {
"0: from all lookup local", "1000: from all lookup main",
"1020: from 2a00:79e1:abc:f604:faac:65ff:fe56:100d/64 lookup 1003"};
std::vector<std::string_view> expected_route_results = {
"", "",
"default via fe80::2a00:79e1:abc:f604 dev wlan0 table 1003 proto "
"ra metric 1024 expires 3335sec hoplimit 64 pref medium"};
ASSERT_EQ(result.result().size(), expected_policy_results.size());
VerifyMatchingResult(result, expected_policy_results, expected_route_results);
}
// Test the case when a packet matches with a policy only input interface of
// which is specified.
TEST_F(ProcessPacketTest, IPv6MatchedByInputInterface) {
const auto ip_family = net_base::IPFamily::kIPv6;
const auto destination_ip =
net_base::IPAddress::CreateFromString("2a00:79e1:abc:f604:200::").value();
const auto source_ip =
net_base::IPAddress::CreateFromString("2401:fa00:480:ee08:100::").value();
auto packet = Packet::CreatePacketForTesting(
ip_family, Packet::Protocol::kTcp, destination_ip, source_ip, 100, 200,
"wlan0");
ASSERT_TRUE(packet);
const auto result = route_manager_.ProcessPacketWithMutation(*packet);
std::vector<std::string_view> expected_policy_results = {
"0: from all lookup local", "1000: from all lookup main",
"1010: from 2401:fa00:480:ee08:20e:c6ff:fe63:5c3f/64 lookup 1002",
"1020: from all iif wlan0 lookup 1003"};
std::vector<std::string_view> expected_route_results = {
"", "", "",
"2a00:79e1:abc:f604::/64 dev wlan0 table 1003 proto kernel metric "
"256 expires 2591735sec pref medium"};
ASSERT_EQ(result.result().size(), expected_policy_results.size());
VerifyMatchingResult(result, expected_policy_results, expected_route_results);
}
// Test the case when a packet matches with a policy which source prefix is
// default and not specified.
TEST_F(ProcessPacketTest, IPv6MatchedByDefault) {
const auto ip_family = net_base::IPFamily::kIPv6;
const auto destination_ip =
net_base::IPAddress::CreateFromString("fdb9:72a:70c5:959d:100::").value();
const auto source_ip =
net_base::IPAddress::CreateFromString("2a00:79e1:abc:f604:10::").value();
auto packet = Packet::CreatePacketForTesting(
ip_family, Packet::Protocol::kUdp, destination_ip, source_ip, 100, 200,
"eth1");
ASSERT_TRUE(packet);
const auto result = route_manager_.ProcessPacketWithMutation(*packet);
std::vector<std::string_view> expected_policy_results = {
"0: from all lookup local", "1000: from all lookup main"};
std::vector<std::string_view> expected_route_results = {
"",
"fdb9:72a:70c5:959d::/64 dev arc_ns1 proto kernel metric 256 pref "
"medium"};
ASSERT_EQ(result.result().size(), expected_policy_results.size());
VerifyMatchingResult(result, expected_policy_results, expected_route_results);
}
// Test the case when no matched route is found.
TEST_F(ProcessPacketTest, IPv6NoMatchedRoute) {
const auto ip_family = net_base::IPFamily::kIPv6;
const auto destination_ip =
net_base::IPAddress::CreateFromString("2a00:79e1:abc:f604:110::").value();
const auto source_ip =
net_base::IPAddress::CreateFromString("2401:fa00:480:ee08:190::").value();
auto packet =
Packet::CreatePacketForTesting(ip_family, Packet::Protocol::kIcmp,
destination_ip, source_ip, 0, 0, "eth1");
ASSERT_TRUE(packet);
const auto result = route_manager_.ProcessPacketWithMutation(*packet);
std::vector<std::string_view> expected_policy_results = {
"0: from all lookup local", "1000: from all lookup main",
"1010: from 2401:fa00:480:ee08:20e:c6ff:fe63:5c3f/64 lookup 1002",
"32765: from all lookup 1002", "32766: from all lookup main"};
std::vector<std::string_view> expected_route_results = {"", "", "", "", ""};
ASSERT_EQ(result.result().size(), expected_policy_results.size());
VerifyMatchingResult(result, expected_policy_results, expected_route_results);
}
} // namespace
} // namespace routing_simulator