From: Chris Down Date: Sat, 15 Nov 2025 17:15:47 +0000 (+0800) Subject: test: Add comprehensive tests for MultiPathRoute device-only syntax X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=23d02f63277c44137fa03af498c4976262df38d2;p=thirdparty%2Fsystemd.git test: Add comprehensive tests for MultiPathRoute device-only syntax Add unit tests for config_parse_multipath_route() covering the new device-only nexthop syntax (@device). Tests verify basic parsing of device-only routes, gateway with device routes, gateway-only routes, interface index handling, etc. Also some checks on new hash/duplicate semantics. Also add an integration test to verify device-only multipath routes are correctly installed in the kernel routing table. --- diff --git a/src/network/test-networkd-conf.c b/src/network/test-networkd-conf.c index e4533737bfc..8120e04f423 100644 --- a/src/network/test-networkd-conf.c +++ b/src/network/test-networkd-conf.c @@ -6,6 +6,8 @@ #include "networkd-address.h" #include "networkd-manager.h" #include "networkd-network.h" +#include "networkd-route-nexthop.h" +#include "ordered-set.h" #include "set.h" #include "strv.h" #include "tests.h" @@ -263,4 +265,163 @@ TEST(config_parse_match_strv) { "KEY3=val with \\quotation\\"))); } +static int parse_mpr(const char *rvalue, OrderedSet **nexthops) { + return config_parse_multipath_route( + "network", "filename", 1, "section", 1, "MultiPathRoute", 0, rvalue, nexthops, NULL); +} + +static void test_config_parse_multipath_route_one(const char *rvalue, int expected_ret, size_t expected_size) { + _cleanup_ordered_set_free_ OrderedSet *nexthops = NULL; + + ASSERT_OK_EQ(parse_mpr(rvalue, &nexthops), expected_ret); + ASSERT_EQ(ordered_set_size(nexthops), expected_size); +} + +static void test_config_parse_multipath_route_verify( + const char *rvalue, + int expected_family, + const char *expected_gw, + const char *expected_ifname, + int expected_ifindex, + uint32_t expected_weight) { + + _cleanup_ordered_set_free_ OrderedSet *nexthops = NULL; + RouteNextHop *nh; + + ASSERT_OK_EQ(parse_mpr(rvalue, &nexthops), 1); + ASSERT_EQ(ordered_set_size(nexthops), 1u); + + ASSERT_NOT_NULL(nh = ordered_set_first(nexthops)); + ASSERT_EQ(nh->family, expected_family); + + if (expected_gw) { + union in_addr_union gw; + ASSERT_OK(in_addr_from_string(expected_family, expected_gw, &gw)); + ASSERT_EQ(memcmp(&nh->gw, &gw, FAMILY_ADDRESS_SIZE(expected_family)), 0); + } else + ASSERT_FALSE(in_addr_is_set(nh->family, &nh->gw)); + + ASSERT_STREQ(nh->ifname, expected_ifname); + + ASSERT_EQ(nh->ifindex, expected_ifindex); + ASSERT_EQ(nh->weight, expected_weight); +} + +TEST(config_parse_multipath_route) { + _cleanup_ordered_set_free_ OrderedSet *nexthops = NULL; + + /* Device only routes */ + test_config_parse_multipath_route_verify("@wg0", AF_UNSPEC, NULL, "wg0", 0, 0); + test_config_parse_multipath_route_verify("@wg0 10", AF_UNSPEC, NULL, "wg0", 0, 9); + test_config_parse_multipath_route_verify("@eth0 255", AF_UNSPEC, NULL, "eth0", 0, 254); + test_config_parse_multipath_route_verify("@1 15", AF_UNSPEC, NULL, NULL, 1, 14); + + /* Gateway with device */ + test_config_parse_multipath_route_verify("10.0.0.1@eth0", AF_INET, "10.0.0.1", "eth0", 0, 0); + test_config_parse_multipath_route_verify("10.0.0.1@eth0 20", AF_INET, "10.0.0.1", "eth0", 0, 19); + test_config_parse_multipath_route_verify("2001:db8::1@wg0 15", AF_INET6, "2001:db8::1", "wg0", 0, 14); + + /* Gateway without device */ + test_config_parse_multipath_route_verify("192.168.1.1", AF_INET, "192.168.1.1", NULL, 0, 0); + test_config_parse_multipath_route_verify("192.168.1.1 100", AF_INET, "192.168.1.1", NULL, 0, 99); + test_config_parse_multipath_route_verify("fe80::1", AF_INET6, "fe80::1", NULL, 0, 0); + + /* Interface index instead of name */ + test_config_parse_multipath_route_verify("10.0.0.1@5", AF_INET, "10.0.0.1", NULL, 5, 0); + test_config_parse_multipath_route_verify("@10", AF_UNSPEC, NULL, NULL, 10, 0); + test_config_parse_multipath_route_verify("@10 50", AF_UNSPEC, NULL, NULL, 10, 49); + + /* Empty value clears nexthops */ + ASSERT_OK_EQ(parse_mpr("@wg0 15", &nexthops), 1); + ASSERT_EQ(ordered_set_size(nexthops), 1u); + ASSERT_OK_EQ(parse_mpr("", &nexthops), 1); + ASSERT_NULL(nexthops); + + /* Make sure device-only/AF_UNSPEC routes do not collapse into a single entry. */ + + /* Different interfaces, same weight */ + ASSERT_OK_EQ(parse_mpr("@wg0 15", &nexthops), 1); + ASSERT_EQ(ordered_set_size(nexthops), 1u); + ASSERT_OK_EQ(parse_mpr("@wg1 15", &nexthops), 1); + ASSERT_EQ(ordered_set_size(nexthops), 2u); + /* Same interface, different weights */ + ASSERT_OK_EQ(parse_mpr("@eth0 10", &nexthops), 1); + ASSERT_OK_EQ(parse_mpr("@eth0 20", &nexthops), 1); + ASSERT_EQ(ordered_set_size(nexthops), 4u); + /* Interface name vs interface index */ + ASSERT_OK_EQ(parse_mpr("@5 15", &nexthops), 1); + ASSERT_EQ(ordered_set_size(nexthops), 5u); + /* Mixing device-only and gateway routes */ + ASSERT_OK_EQ(parse_mpr("10.0.0.1@eth0 20", &nexthops), 1); + ASSERT_OK_EQ(parse_mpr("192.168.1.1 30", &nexthops), 1); + ASSERT_EQ(ordered_set_size(nexthops), 7u); + /* Large interface index */ + ASSERT_OK_EQ(parse_mpr("@999999 10", &nexthops), 1); + ASSERT_EQ(ordered_set_size(nexthops), 8u); + /* IPv4 and IPv6 mixing */ + ASSERT_OK_EQ(parse_mpr("2001:db8::1@eth0 20", &nexthops), 1); + ASSERT_EQ(ordered_set_size(nexthops), 9u); + /* Default weight handling */ + ASSERT_OK_EQ(parse_mpr("@wg2", &nexthops), 1); + ASSERT_OK_EQ(parse_mpr("@wg3", &nexthops), 1); + ASSERT_EQ(ordered_set_size(nexthops), 11u); + + nexthops = ordered_set_free(nexthops); + + /* Insertion order does not affect deduplication */ + _cleanup_ordered_set_free_ OrderedSet *nexthops2 = NULL; + ASSERT_OK_EQ(parse_mpr("@wg0 10", &nexthops), 1); + ASSERT_OK_EQ(parse_mpr("@wg1 20", &nexthops), 1); + ASSERT_OK_EQ(parse_mpr("@wg1 20", &nexthops2), 1); + ASSERT_OK_EQ(parse_mpr("@wg0 10", &nexthops2), 1); + ASSERT_EQ(ordered_set_size(nexthops), 2u); + ASSERT_EQ(ordered_set_size(nexthops2), 2u); + + nexthops = ordered_set_free(nexthops); + + /* Duplicate routes should be detected and rejected with a warning. */ + + /* Exact duplicates are rejected */ + ASSERT_OK_EQ(parse_mpr("@wg0 15", &nexthops), 1); + ASSERT_EQ(ordered_set_size(nexthops), 1u); + ASSERT_OK_EQ(parse_mpr("@wg0 15", &nexthops), 0); + ASSERT_EQ(ordered_set_size(nexthops), 1u); + + nexthops = ordered_set_free(nexthops); + + /* Default weight vs explicit weight=1 are treated as identical */ + ASSERT_OK_EQ(parse_mpr("@eth0", &nexthops), 1); + ASSERT_EQ(ordered_set_size(nexthops), 1u); + ASSERT_OK_EQ(parse_mpr("@eth0 1", &nexthops), 0); + ASSERT_EQ(ordered_set_size(nexthops), 1u); + + nexthops = ordered_set_free(nexthops); + + /* Weight=1 then default weight (reverse order), still detected as duplicate */ + ASSERT_OK_EQ(parse_mpr("@wg0 1", &nexthops), 1); + ASSERT_EQ(ordered_set_size(nexthops), 1u); + ASSERT_OK_EQ(parse_mpr("@wg0", &nexthops), 0); + ASSERT_EQ(ordered_set_size(nexthops), 1u); + + nexthops = ordered_set_free(nexthops); + + /* Device-only vs gateway with device are semantically distinct, both accepted */ + ASSERT_OK_EQ(parse_mpr("@eth0 10", &nexthops), 1); + ASSERT_OK_EQ(parse_mpr("10.0.0.1@eth0 10", &nexthops), 1); + ASSERT_EQ(ordered_set_size(nexthops), 2u); + + /* Invalid input should be rejected */ + + /* Invalid gateway addresses */ + test_config_parse_multipath_route_one("999.999.999.999", 0, 0); + test_config_parse_multipath_route_one("not-an-ip", 0, 0); + test_config_parse_multipath_route_one("10", 0, 0); + + /* Invalid weights */ + test_config_parse_multipath_route_one("@wg0 0", 0, 0); /* Weight 0 */ + test_config_parse_multipath_route_one("@wg0 257", 0, 0); /* Weight > 256 */ + test_config_parse_multipath_route_one("@wg0 -1", 0, 0); /* Negative */ + test_config_parse_multipath_route_one("@wg0 abc", 0, 0); /* Non-numeric */ +} + DEFINE_TEST_MAIN(LOG_INFO); diff --git a/test/test-network/conf/25-route-static.network b/test/test-network/conf/25-route-static.network index 5ddd7de61a5..8e0578f0584 100644 --- a/test/test-network/conf/25-route-static.network +++ b/test/test-network/conf/25-route-static.network @@ -115,3 +115,8 @@ Peer=1.1.8.104/31 [Route] Gateway=1.1.8.104 + +[Route] +Destination=192.168.20.0/24 +MultiPathRoute=@test1 15 +MultiPathRoute=@dummy98 25 diff --git a/test/test-network/systemd-networkd-tests.py b/test/test-network/systemd-networkd-tests.py index beaefbdc895..61bc8b75bf8 100755 --- a/test/test-network/systemd-networkd-tests.py +++ b/test/test-network/systemd-networkd-tests.py @@ -4391,6 +4391,13 @@ class NetworkdNetworkTests(unittest.TestCase, Utilities): self.assertIn('via 2001:1234:5:8fff:ff:ff:ff:ff dev dummy98', output) self.assertIn('via 2001:1234:5:9fff:ff:ff:ff:ff dev dummy98', output) + print('### ip route show 192.168.20.0/24') + output = check_output('ip route show 192.168.20.0/24') + print(output) + self.assertIn('192.168.20.0/24 proto static', output) + self.assertIn('nexthop dev test1 weight 15', output) + self.assertIn('nexthop dev dummy98 weight 25', output) + check_json(networkctl_json()) def _check_unreachable_routes_removed(self):