]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
test: Add comprehensive tests for MultiPathRoute device-only syntax 39742/head
authorChris Down <chris@chrisdown.name>
Sat, 15 Nov 2025 17:15:47 +0000 (01:15 +0800)
committerChris Down <chris@chrisdown.name>
Fri, 28 Nov 2025 11:09:15 +0000 (19:09 +0800)
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.

src/network/test-networkd-conf.c
test/test-network/conf/25-route-static.network
test/test-network/systemd-networkd-tests.py

index e4533737bfc95001872bdfbeb895d27492e2d810..8120e04f4239cc2fd308bca210a6c26aee86378f 100644 (file)
@@ -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);
index 5ddd7de61a5b1bd1c596bf8fe42715b9fdb3af31..8e0578f0584dce19070c4ea95cb0fbaee45f6f58 100644 (file)
@@ -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
index beaefbdc8957259f30169ed9875017a2e29f22d5..61bc8b75bf8f5acdfd8bf77a9fc5d0e5a609581d 100755 (executable)
@@ -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):