network: Support interface-bound ECMP routes in MultiPathRoute=

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:
Chris Down
2025-11-28 19:09:15 +08:00
parent 68ce283c3f
commit 645b709a38
2 changed files with 57 additions and 38 deletions
+12 -5
View File
@@ -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>
+45 -33
View File
@@ -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)) {