diff --git a/man/systemd.network.xml b/man/systemd.network.xml index b9f49a43a8..b00c2c6795 100644 --- a/man/systemd.network.xml +++ b/man/systemd.network.xml @@ -2325,13 +2325,20 @@ NFTSet=prefix:netdev:filter:eth_ipv4_prefix - MultiPathRoute=address[@name] [weight] + MultiPathRoute= Configures multipath route. Multipath routing is the technique of using multiple - alternative paths through a network. Takes gateway address. Optionally, takes a network - interface name or index separated with @, and a weight in 1..256 for this - multipath route separated with whitespace. This setting can be specified multiple times. If - an empty string is assigned, then the all previous assignments are cleared. + alternative paths through a network. Takes a gateway address and/or a network interface + name or index (prefixed with @). At least one of these must be specified. + Optionally, a weight in 1..256 can be specified, separated with whitespace. This setting + can be specified multiple times. If an empty string is assigned, then all previous + assignments are cleared. + + Examples: + MultiPathRoute=10.0.0.1@eth0 20 +MultiPathRoute=192.168.1.1 50 +MultiPathRoute=@wg0 15 +MultiPathRoute=2001:db8::1@eth0 diff --git a/src/network/networkd-route-nexthop.c b/src/network/networkd-route-nexthop.c index 9f5970270e..bebb401b33 100644 --- a/src/network/networkd-route-nexthop.c +++ b/src/network/networkd-route-nexthop.c @@ -94,10 +94,12 @@ static void route_nexthop_hash_func_full(const RouteNextHop *nh, struct siphash /* See nh_comp() in net/ipv4/fib_semantics.c of the kernel. */ siphash24_compress_typesafe(nh->family, state); - if (!IN_SET(nh->family, AF_INET, AF_INET6)) - return; - in_addr_hash_func(&nh->gw, nh->family, state); + /* For device-only nexthops parsed from config, family is AF_UNSPEC until verification. + * We still need to hash weight/ifindex/ifname to distinguish different device-only entries. */ + if (IN_SET(nh->family, AF_INET, AF_INET6)) + in_addr_hash_func(&nh->gw, nh->family, state); + if (with_weight) siphash24_compress_typesafe(nh->weight, state); siphash24_compress_typesafe(nh->ifindex, state); @@ -115,12 +117,13 @@ static int route_nexthop_compare_func_full(const RouteNextHop *a, const RouteNex if (r != 0) return r; - if (!IN_SET(a->family, AF_INET, AF_INET6)) - return 0; - - r = memcmp(&a->gw, &b->gw, FAMILY_ADDRESS_SIZE(a->family)); - if (r != 0) - return r; + /* For device-only nexthops parsed from config, family is AF_UNSPEC until verification. + * We still need to compare weight/ifindex/ifname to distinguish different device-only entries. */ + if (IN_SET(a->family, AF_INET, AF_INET6)) { + r = memcmp(&a->gw, &b->gw, FAMILY_ADDRESS_SIZE(a->family)); + if (r != 0) + return r; + } if (with_weight) { r = CMP(a->weight, b->weight); @@ -553,30 +556,34 @@ static int append_nexthop_one(const Route *route, const RouteNextHop *nh, struct (*rta)->rta_len += sizeof(struct rtnexthop); - if (nh->family == route->family) { - r = rtattr_append_attribute(rta, RTA_GATEWAY, &nh->gw, FAMILY_ADDRESS_SIZE(nh->family)); - if (r < 0) - goto clear; + /* For device-only nexthops, skip RTA_GATEWAY entirely. The kernel will use the + * interface specified in rtnh_ifindex without requiring a gateway address. */ + if (in_addr_is_set(nh->family, &nh->gw)) { + if (nh->family == route->family) { + r = rtattr_append_attribute(rta, RTA_GATEWAY, &nh->gw, FAMILY_ADDRESS_SIZE(nh->family)); + if (r < 0) + goto clear; - rtnh = (struct rtnexthop *)((uint8_t *) *rta + offset); - rtnh->rtnh_len += RTA_SPACE(FAMILY_ADDRESS_SIZE(nh->family)); + rtnh = (struct rtnexthop *)((uint8_t *) *rta + offset); + rtnh->rtnh_len += RTA_SPACE(FAMILY_ADDRESS_SIZE(nh->family)); - } else if (nh->family == AF_INET6) { - assert(route->family == AF_INET); + } else if (nh->family == AF_INET6) { + assert(route->family == AF_INET); - r = rtattr_append_attribute(rta, RTA_VIA, - &(RouteVia) { - .family = nh->family, - .address = nh->gw, - }, sizeof(RouteVia)); - if (r < 0) - goto clear; + r = rtattr_append_attribute(rta, RTA_VIA, + &(RouteVia) { + .family = nh->family, + .address = nh->gw, + }, sizeof(RouteVia)); + if (r < 0) + goto clear; - rtnh = (struct rtnexthop *)((uint8_t *) *rta + offset); - rtnh->rtnh_len += RTA_SPACE(sizeof(RouteVia)); + rtnh = (struct rtnexthop *)((uint8_t *) *rta + offset); + rtnh->rtnh_len += RTA_SPACE(sizeof(RouteVia)); - } else if (nh->family == AF_INET) - assert_not_reached(); + } else if (nh->family == AF_INET) + assert_not_reached(); + } return 0; @@ -1080,11 +1087,16 @@ int config_parse_multipath_route( } } - r = in_addr_from_string_auto(word, &nh->family, &nh->gw); - if (r < 0) { - log_syntax(unit, LOG_WARNING, filename, line, r, - "Invalid multipath route gateway '%s', ignoring assignment: %m", rvalue); - return 0; + if (isempty(word)) { + if (!dev) + return log_syntax_parse_error(unit, filename, line, SYNTHETIC_ERRNO(EINVAL), lvalue, rvalue); + } else { + r = in_addr_from_string_auto(word, &nh->family, &nh->gw); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Invalid multipath route gateway '%s', ignoring assignment: %m", rvalue); + return 0; + } } if (!isempty(p)) { diff --git a/src/network/test-networkd-conf.c b/src/network/test-networkd-conf.c index ddb8fcaa36..00791cea91 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 5ddd7de61a..8e0578f058 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 eb3911e066..2843be3e2a 100755 --- a/test/test-network/systemd-networkd-tests.py +++ b/test/test-network/systemd-networkd-tests.py @@ -4392,6 +4392,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):