From: Chris Down Date: Fri, 14 Nov 2025 16:52:51 +0000 (+0800) Subject: network: Support interface-bound ECMP routes in MultiPathRoute= X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=645b709a383a475fae434acb0b4eb0558ecd2b15;p=thirdparty%2Fsystemd.git 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. --- diff --git a/man/systemd.network.xml b/man/systemd.network.xml index b9f49a43a80..b00c2c67957 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 5ad8cd4c648..7eb1a794247 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)) {