network: Support interface-bound ECMP routes in MultiPathRoute= (#39742)
MultiPathRoute= can now specify device-only nexthops without a gateway address, e.g. MultiPathRoute=@wg0. This enables ECMP configurations over interfaces that don't use gateway addresses, such as WireGuard tunnels. The syntax is extended from "address[@device] [weight]" to "[address]@device [weight]". The address is now optional, but at least one of gateway or device must be specified. The @ symbol must still be present for device-only routes, making the syntax unambiguous: @wg0 specifies a device, while a bare IP address specifies a gateway. Device-only nexthops are only available for IPv4 routes. Device-only multipath routes for IPv6 are not supported by the kernel's netlink interface and will be rejected with a warning. This change is fully backwards compatible. All existing configurations continue to work unchanged, as they always included a gateway address. Closes #39699.
This commit is contained in:
+12
-5
@@ -2325,13 +2325,20 @@ NFTSet=prefix:netdev:filter:eth_ipv4_prefix</programlisting>
|
||||
</varlistentry>
|
||||
|
||||
<varlistentry>
|
||||
<term><varname>MultiPathRoute=<replaceable>address</replaceable>[@<replaceable>name</replaceable>] [<replaceable>weight</replaceable>]</varname></term>
|
||||
<term><varname>MultiPathRoute=</varname></term>
|
||||
<listitem>
|
||||
<para>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 <literal>@</literal>, 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.</para>
|
||||
alternative paths through a network. Takes a gateway address and/or a network interface
|
||||
name or index (prefixed with <literal>@</literal>). 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.</para>
|
||||
|
||||
<para>Examples:</para>
|
||||
<programlisting>MultiPathRoute=10.0.0.1@eth0 20
|
||||
MultiPathRoute=192.168.1.1 50
|
||||
MultiPathRoute=@wg0 15
|
||||
MultiPathRoute=2001:db8::1@eth0</programlisting>
|
||||
|
||||
<xi:include href="version-info.xml" xpointer="v245"/>
|
||||
</listitem>
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user