]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
network: Support interface-bound ECMP routes in MultiPathRoute=
authorChris Down <chris@chrisdown.name>
Fri, 14 Nov 2025 16:52:51 +0000 (00:52 +0800)
committerChris Down <chris@chrisdown.name>
Fri, 28 Nov 2025 11:09:15 +0000 (19:09 +0800)
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.

man/systemd.network.xml
src/network/networkd-route-nexthop.c

index b9f49a43a804ad300e95919be3e7173416b37beb..b00c2c67957029d6a30dd071a0355ea49b555abe 100644 (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>
index 5ad8cd4c6485ff8c392a9ce81716f25de213cd18..7eb1a79424758852b558c15f53c3c50d676bed57 100644 (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)) {